1import * as agw2 from "@distilled.cloud/aws/apigatewayv2";
2import type * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import type { Input } from "../../Input.ts";
5import * as Namespace from "../../Namespace.ts";
6import * as Output from "../../Output.ts";
7import { Permission } from "../Lambda/Permission.ts";
8import { Api } from "./Api.ts";
9import { Integration } from "./Integration.ts";
10import { Route } from "./Route.ts";
11import { Stage } from "./Stage.ts";
12
13/**
14 * The Lambda function an {@link HttpApi} fronts. Structural on purpose —
15 * any resource exposing `functionArn`/`functionName` outputs (an
16 * `AWS.Lambda.Function`) qualifies.
17 */
18export interface HttpApiHandler {
19 /** The logical ID of the handler function. */
20 readonly LogicalId: string;
21 /** ARN of the Lambda function that serves requests. */
22 readonly functionArn: Output.Output<string>;
23 /** Name of the Lambda function that serves requests. */
24 readonly functionName: Output.Output<string>;
25}
26
27export interface HttpApiProps {
28 /**
29 * The Lambda function that serves every request (`$default` route,
30 * `AWS_PROXY` integration, payload format 2.0 — the same event shape as
31 * Lambda Function URLs).
32 */
33 handler: HttpApiHandler;
34 /**
35 * Name of the API. If omitted, Alchemy generates a deterministic
36 * physical name.
37 */
38 name?: Input<string>;
39 /** Description of the API. */
40 description?: Input<string>;
41 /** CORS configuration. */
42 cors?: Input<agw2.Cors>;
43 /**
44 * Disable the default `execute-api` endpoint (serve only via custom
45 * domains).
46 */
47 disableExecuteApiEndpoint?: Input<boolean>;
48 /**
49 * The stage name to deploy. `$default` serves at the API root.
50 * @default "$default"
51 */
52 stageName?: Input<string>;
53 /**
54 * Integration timeout (e.g. `"29 seconds"`; a bare number is
55 * milliseconds, 50–30000). Sent to the API in whole milliseconds
56 * (`TimeoutInMillis`).
57 * @default 30 seconds
58 */
59 timeout?: Input<Duration.Input>;
60 /** User-defined tags for the API and stage. */
61 tags?: Record<string, string>;
62}
63
64/**
65 * The flagship HTTP API → Lambda front door: composes an {@link Api}
66 * (HTTP), an `AWS_PROXY` {@link Integration} (payload 2.0), a `$default`
67 * {@link Route}, an auto-deployed {@link Stage}, and the API Gateway
68 * invoke `Permission` in one call.
69 *
70 * The Lambda receives the same event shape as a Function URL, so an
71 * Effect-native `fetch` handler works unchanged behind the API.
72 *
73 * ### Creating an HTTP API
74 * **Example:** Front a Lambda function
75 * ```typescript
76 * const fn = yield* MyFunction;
77 * const { url } = yield* ApiGatewayV2.HttpApi("Api", { handler: fn });
78 * // url -> https://{apiId}.execute-api.{region}.amazonaws.com
79 * ```
80 *
81 * **Example:** With CORS and a named stage
82 * ```typescript
83 * const { api, stage, url } = yield* ApiGatewayV2.HttpApi("Api", {
84 * handler: fn,
85 * stageName: "prod",
86 * cors: { AllowOrigins: ["*"], AllowMethods: ["*"] },
87 * });
88 * ```
89 */
90export const HttpApi = (id: string, props: HttpApiProps) =>
91 Namespace.push(
92 id,
93 Effect.gen(function* () {
94 const api = yield* Api("Api", {
95 name: props.name,
96 protocolType: "HTTP",
97 description: props.description,
98 corsConfiguration: props.cors,
99 disableExecuteApiEndpoint: props.disableExecuteApiEndpoint,
100 tags: props.tags,
101 } as any);
102
103 const integration = yield* Integration("Integration", {
104 api,
105 integrationType: "AWS_PROXY",
106 integrationUri: props.handler.functionArn,
107 payloadFormatVersion: "2.0",
108 timeout: props.timeout,
109 });
110
111 yield* Route("Default", {
112 api,
113 routeKey: "$default",
114 integration,
115 });
116
117 const stage = yield* Stage("Stage", {
118 api,
119 stageName: props.stageName ?? "$default",
120 autoDeploy: true,
121 tags: props.tags,
122 });
123
124 yield* Permission("Permission", {
125 action: "lambda:InvokeFunction",
126 functionName: props.handler.functionName,
127 principal: "apigateway.amazonaws.com",
128 sourceArn: Output.map(
129 Output.all(props.handler.functionArn, api.apiId),
130 ([fnArn, apiId]: [string, string]) => {
131 const [, , , region, accountId] = fnArn.split(":");
132 return `arn:aws:execute-api:${region}:${accountId}:${apiId}/*`;
133 },
134 ),
135 } as any);
136
137 return {
138 /** The underlying HTTP {@link Api}. */
139 api,
140 /** The underlying {@link Integration}. */
141 integration,
142 /** The auto-deployed {@link Stage}. */
143 stage,
144 /** The URL clients call. */
145 url: stage.invokeUrl,
146 };
147 }),
148 );