Header menu logo FsCDK

API Gateway Designing world-class HTTP APIs with FsCDK

Amazon API Gateway HTTP APIs deliver the latency, pricing, and simplicity that modern serverless teams expect. This notebook combines FsCDK builders with practices championed by AWS Heroes Jeremy Daly, Yan Cui, and the API Gateway product team, so you can publish secure, observable endpoints that scale from prototype to production.

Key influences - Jeremy Daly – Serverless Chats Ep.133 (4.9★ community rating) for event-driven API design patterns. - Yan Cui – “HTTP APIs best practices” blog series for cold-start minimisation and auth flows. - re:Invent ARC406 – Building resilient APIs with API Gateway for real-world resiliency playbooks.

Use the code samples alongside the implementation checklist and the “Further learning” section at the end to deepen your expertise.

Quick start blueprint

#r "../src/bin/Release/net8.0/publish/Amazon.JSII.Runtime.dll"
#r "../src/bin/Release/net8.0/publish/Constructs.dll"
#r "../src/bin/Release/net8.0/publish/Amazon.CDK.Lib.dll"
#r "../src/bin/Release/net8.0/publish/FsCDK.dll"

open FsCDK
open Amazon.CDK
open Amazon.CDK.AWS.Apigatewayv2
//open Amazon.CDK.AWS.Apigatewayv2Integrations
open Amazon.CDK.AWS.Lambda

Basic HTTP API

Create a simple HTTP API with auto-deployment enabled by default.

stack "BasicHttpAPI" {
    // Create the HTTP API
    let api = httpApi "my-api" { description "My HTTP API" }
    ()
}

HTTP API with CORS

Adopt a least-privilege CORS policy from day one. The development configuration below mirrors the rapid-prototyping approach that Heitor Lessa demonstrates in the Powertools workshops, while the production configuration reflects the lock-down guidance from the AWS Security Blog article “Implementing secure CORS for API Gateway.” Use permissive settings only in sandbox environments and document every approved origin for production.

stack "HttpAPIWithCORS" {
    // Development: Allow all origins
    let devApi =
        httpApi "dev-api" {
            description "Development API with permissive CORS"
            cors (HttpApiHelpers.corsPermissive ())
            ()
        }

    // Production: Restrict to specific origins
    let corsOptions =
        HttpApiHelpers.cors
            [ "https://myapp.com"; "https://www.myapp.com" ]
            [ CorsHttpMethod.GET; CorsHttpMethod.POST; CorsHttpMethod.PUT ]
            [ "Content-Type"; "Authorization" ]

    let prodApi =
        httpApi "prod-api" {
            description "Production API with restricted CORS"
            cors corsOptions
        }

    ()
}

HTTP API with Lambda Integration

FsCDK keeps the Lambda integration lightweight so you can focus on business logic. Follow the playbook shared in re:Invent SVS402 – Deep dive on serverless APIs: separate route definitions from function code, enable structured logging via Powertools, and surface integration metrics in CloudWatch. The snippet below establishes the foundation; pair it with reserved concurrency and DLQs as shown in Lambda Production Defaults to hit production-readiness quickly.

stack "HttpAPIWithLambda" {
    // Create Lambda function
    let apiFunction =
        lambda "ApiHandler" {
            runtime Runtime.DOTNET_8
            handler "Api::Handler"
            code "./lambda"
            environment [ "API_VERSION", "v1" ]
            ()
        }

    // Create HTTP API
    let api =
        httpApi "users-api" {
            description "Users API with Lambda integration"
            cors (HttpApiHelpers.corsPermissive ())
        }

    // Note: Route integrations must be added using the CDK HttpApi directly
    // Example:
    //   let integration = HttpLambdaIntegration("GetUsersIntegration", apiFunction.Function.Value)
    //   api.Api.AddRoutes(HttpRouteOptions(
    //       Path = "/users",
    //       Methods = [| HttpMethod.GET |],
    //       Integration = integration
    //   ))

    ()
}

Multiple routes & harmonised contracts

Model your REST surface explicitly so consumers always know which verbs and payloads are supported. This mirrors the contract-first workflow advocated by Jeremy Daly in his “EventBridge and API Gateway integration” talks—each route should map cleanly to a Lambda function or integration with clear telemetry. The code below introduces per-verb handlers; augment it with JSON schema validators or Lambda Powertools’ middleware for request/response shaping.

stack "MultiRouteAPI" {
    // Create Lambda functions for different operations
    let getUsersFunc =
        lambda "GetUsers" {
            runtime Runtime.DOTNET_8
            handler "Api::GetUsers"
            code "./lambda"
            ()
        }

    let createUserFunc =
        lambda "CreateUser" {
            runtime Runtime.DOTNET_8
            handler "Api::CreateUser"
            code "./lambda"
        }

    let updateUserFunc =
        lambda "UpdateUser" {
            runtime Runtime.DOTNET_8
            handler "Api::UpdateUser"
            code "./lambda"
        }

    let deleteUserFunc =
        lambda "DeleteUser" {
            runtime Runtime.DOTNET_8
            handler "Api::DeleteUser"
            code "./lambda"
        }

    // Create HTTP API
    let api =
        httpApi "rest-api" {
            description "RESTful API with multiple routes"
            cors (HttpApiHelpers.corsPermissive ())
        }

    // Routes are added using CDK directly:
    // GET /users - List users
    // POST /users - Create user
    // PUT /users/{id} - Update user
    // DELETE /users/{id} - Delete user`
    ()
}

Custom domains & TLS

Serve your API from branded domains to simplify client configuration and enforce HSTS. Provision ACM certificates in the target region (us-east-1 for edge-optimised) and map them via Route 53 alias records, following the detailed steps in the AWS Networking Blog post “End-to-end TLS with API Gateway.”

Authorization strategies

Plan for authentication and authorisation from day zero. HTTP APIs natively support JWT authorisers (ideal for Cognito, Auth0, or custom OIDC providers) and Lambda authorisers for bespoke logic. Map your requirements to the guidance from Ben Kehoe’s “Identity for serverless” series and the official API Gateway Security Workshops so you can implement least-privilege, auditable access.

Helper functions

FsCDK ships ergonomic helpers for CORS policies and route keys so you can codify conventions instead of scattering strings through your codebase. Use them to mirror the patterns from the AWS API Gateway Workshop—define reusable builders, enforce consistent verbs and paths, and keep policy changes centralised.

// CORS Helpers
let permissiveCors = HttpApiHelpers.corsPermissive ()

let restrictedCors =
    HttpApiHelpers.cors
        [ "https://example.com" ]
        [ CorsHttpMethod.GET; CorsHttpMethod.POST ]
        [ "Content-Type"; "Authorization" ]

// Route Key Helpers
let getUsersRoute = HttpApiHelpers.getRoute "/users"
let createUserRoute = HttpApiHelpers.postRoute "/users"
let updateUserRoute = HttpApiHelpers.putRoute "/users/{id}"
let deleteUserRoute = HttpApiHelpers.deleteRoute "/users/{id}"
let anyMethodRoute = HttpApiHelpers.anyRoute "/{proxy+}"

Implementation checklist

Area

What to do

Why

Latency

Choose HTTP APIs over REST APIs for 50–70% lower latency and cost. Keep payloads small and enable compression when responses exceed 1 MB.

Aligns with re:Invent ARC406 recommendations and AWS’ pricing model.

Security

Lock CORS to approved origins, prefer JWT authorisers for stateless auth, and front public APIs with AWS WAF. Enable access logging to CloudWatch Logs Insights.

Mirrors Ben Kehoe’s least-privilege guidance and AWS Security Blog best practices.

Reliability

Set Lambda timeouts ≤29 s, configure reserved concurrency, and attach DLQs or on-failure destinations for integrations. Add CloudWatch alarms on 4xx/5xx metrics.

Matches the resiliency playbook from AWS Builders Library – Automating safe deployments.

Cost

Monitor $default stage metrics, use pay-per-request billing, and cache responses at the integration layer where possible. Audit usage quarterly.

Reinforces advice from Serverless Land cost optimisation sessions.

Operations

Tag APIs (Service, Environment), version routes (/v1, /v2), and document schemas using OpenAPI. Include runbooks for authoriser failures.

Supports operational excellence per Well-Architected Serverless Lens.

FsCDK’s builder defaults—auto-deployed $default stage, throttling, and disabled CORS—provide a sensible starting point. Use the escape hatch (api.Api) whenever you need to customise integrations, authorisers, or stages.

// Example escape hatch usage (schema validation, authorisers, etc.)
// let integration = HttpLambdaIntegration("UsersIntegration", handler.Function.Value)
// api.Api.AddRoutes(HttpRouteOptions(Path = "/users", Methods = [| HttpMethod.GET |], Integration = integration))

Further learning

Combine these resources with the FsCDK notebooks (Lambda Production Defaults, EventBridge, IAM Best Practices) to deliver secure, observable APIs with confidence.

namespace FsCDK
namespace Amazon
namespace Amazon.CDK
namespace Amazon.CDK.AWS
namespace Amazon.CDK.AWS.Apigatewayv2
namespace Amazon.CDK.AWS.Lambda
val stack: name: string -> StackBuilder
<summary>Creates an AWS CDK Stack construct.</summary>
<param name="name">The name of the stack.</param>
<code lang="fsharp"> stack "MyStack" { lambda myFunction bucket myBucket } </code>
val api: HttpApiResource
val httpApi: name: string -> HttpApiBuilder
<summary> Creates a new HTTP API builder with secure defaults. Example: httpApi "my-api" { description "My API"; cors (HttpApiHelpers.corsPermissive ()) } </summary>
custom operation: description (string) Calls HttpApiBuilder.Description
val devApi: HttpApiResource
custom operation: cors (CorsPreflightOptions) Calls HttpApiBuilder.Cors
module HttpApiHelpers from FsCDK
<summary> Helper functions for creating HTTP API CORS configurations </summary>
val corsPermissive: unit -> CorsPreflightOptions
<summary> Creates permissive CORS for development (allows all origins, methods, headers) </summary>
val corsOptions: CorsPreflightOptions
val cors: allowOrigins: string list -> allowMethods: CorsHttpMethod list -> allowHeaders: string list -> CorsPreflightOptions
<summary> Creates CORS preflight options with common defaults </summary>
[<Struct>] type CorsHttpMethod = | ANY = 0 | DELETE = 1 | GET = 2 | HEAD = 3 | OPTIONS = 4 | PATCH = 5 | POST = 6 | PUT = 7
field CorsHttpMethod.GET: CorsHttpMethod = 2
field CorsHttpMethod.POST: CorsHttpMethod = 6
field CorsHttpMethod.PUT: CorsHttpMethod = 7
val prodApi: HttpApiResource
val apiFunction: FunctionSpec
val lambda: name: string -> FunctionBuilder
<summary>Creates a Lambda function configuration.</summary>
<param name="name">The function name.</param>
<code lang="fsharp"> lambda "MyFunction" { handler "index.handler" runtime Runtime.NODEJS_18_X code "./lambda" timeout 30.0 } </code>
custom operation: runtime (Runtime) Calls FunctionBuilder.Runtime
<summary>Sets the runtime for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="runtime">The Lambda runtime.</param>
<code lang="fsharp"> lambda "MyFunction" { runtime Runtime.NODEJS_18_X } </code>
Multiple items
type Runtime = inherit DeputyBase new: name: string * ?family: Nullable<RuntimeFamily> * ?props: ILambdaRuntimeProps -> unit member RuntimeEquals: other: Runtime -> bool member ToString: unit -> string member BundlingImage: DockerImage member Family: Nullable<RuntimeFamily> member IsVariable: bool member Name: string member SupportsCodeGuruProfiling: bool member SupportsInlineCode: bool ...

--------------------
Runtime(name: string, ?family: System.Nullable<RuntimeFamily>, ?props: ILambdaRuntimeProps) : Runtime
property Runtime.DOTNET_8: Runtime with get
custom operation: handler (string) Calls FunctionBuilder.Handler
<summary>Sets the handler for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="handler">The handler name (e.g., "index.handler").</param>
<code lang="fsharp"> lambda "MyFunction" { handler "index.handler" } </code>
custom operation: code (Code) Calls FunctionBuilder.Code
<summary>Sets the code source from a Code object.</summary>
<param name="config">The function configuration.</param>
<param name="path">The Code object.</param>
<code lang="fsharp"> lambda "MyFunction" { code (Code.FromBucket myBucket "lambda.zip") } </code>
custom operation: environment ((string * string) list) Calls FunctionBuilder.Environment
<summary>Sets environment variables for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="env">List of key-value pairs for environment variables.</param>
<code lang="fsharp"> lambda "MyFunction" { environment [ "KEY1", "value1"; "KEY2", "value2" ] } </code>
val getUsersFunc: FunctionSpec
val createUserFunc: FunctionSpec
val updateUserFunc: FunctionSpec
val deleteUserFunc: FunctionSpec
val permissiveCors: CorsPreflightOptions
val restrictedCors: CorsPreflightOptions
val getUsersRoute: HttpRouteKey
val getRoute: path: string -> HttpRouteKey
<summary> Creates an HTTP route key for GET requests </summary>
val createUserRoute: HttpRouteKey
val postRoute: path: string -> HttpRouteKey
<summary> Creates an HTTP route key for POST requests </summary>
val updateUserRoute: HttpRouteKey
val putRoute: path: string -> HttpRouteKey
<summary> Creates an HTTP route key for PUT requests </summary>
val deleteUserRoute: HttpRouteKey
val deleteRoute: path: string -> HttpRouteKey
<summary> Creates an HTTP route key for DELETE requests </summary>
val anyMethodRoute: HttpRouteKey
val anyRoute: path: string -> HttpRouteKey
<summary> Creates an HTTP route key for any method </summary>

Type something to start searching.