Skip to content
AWS aws serverless 5 min read

Building a Serverless API (API Gateway + Lambda)

A serverless API lets you expose code over HTTP without running or patching any servers. You write a small function, point a public URL at it, and AWS handles the scaling, availability, and billing-per-request. In this page we build a working REST endpoint end to end: a Lambda function (your code), an HTTP API in API Gateway (the public front door), the proxy integration that connects them, a deploy, and a real test of the live URL. We do every step twice, once in the AWS Management Console and once with the AWS CLI (the command-line tool), so you can pick whichever fits your workflow.

The pieces and how they fit together

Three things make a serverless API work:

  • Lambda (AWS’s function-as-a-service product) runs your code on demand. No server to manage.
  • API Gateway is a managed service that gives your function a public HTTPS URL, handles routing, and can do auth, throttling, and CORS.
  • The proxy integration is the wiring between them. It forwards the whole HTTP request to your function as a JSON event, and turns your function’s return value back into an HTTP response.

HTTP API vs REST API: API Gateway offers two flavors. HTTP APIs are newer, cheaper (about $1.00 per million requests vs $3.50 for REST APIs in 2026), and faster to set up — use them for most new work. Pick a REST API only when you need older features like request/response validation, API keys with usage plans, or AWS WAF tie-ins. We use an HTTP API here.

Step 1 — create the Lambda function

The function receives an event (the incoming request) and returns a response object. With proxy integration, that object must have an exact shape: statusCode, optional headers, and a body string. Get this wrong and the caller gets a 502 Bad Gateway.

Save this as index.mjs (Node.js 22 runtime, the current default in 2026):

export const handler = async (event) => {
  const name = event.queryStringParameters?.name ?? "world";
  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message: `Hello, ${name}!` }),
  };
};

Console steps:

  1. Open the Lambda console and click Create function.
  2. Choose Author from scratch, name it hello-api, runtime Node.js 22.x, architecture arm64 (cheaper and faster).
  3. Click Create function, paste the code above into the editor, and click Deploy.

CLI: zip the file and create the function. The --role is the function’s execution role (an IAM identity that grants the function its permissions — see the execution role page).

zip function.zip index.mjs

aws lambda create-function \
  --function-name hello-api \
  --runtime nodejs22.x \
  --architectures arm64 \
  --handler index.handler \
  --zip-file fileb://function.zip \
  --role arn:aws:iam::111122223333:role/hello-api-role

Output:

{
    "FunctionName": "hello-api",
    "FunctionArn": "arn:aws:lambda:us-east-1:111122223333:function:hello-api",
    "Runtime": "nodejs22.x",
    "Handler": "index.handler",
    "State": "Pending"
}

Step 2 — create the HTTP API with a route and proxy integration

A route is a method + path (like GET /hello) that API Gateway matches against incoming requests. Each route points to an integration — here, a Lambda proxy integration.

Console steps:

  1. Open the API Gateway console and click Create APIHTTP APIBuild.
  2. Under Integrations, choose Lambda and select your hello-api function. Name the API hello-http-api.
  3. Click Next. Set the route to method GET, path /hello.
  4. Leave the stage as $default with auto-deploy on, then click Create.

CLI: the one-shot create-api form creates the API, a $default catch-all route, the integration, and the stage in a single call:

aws apigatewayv2 create-api \
  --name hello-http-api \
  --protocol-type HTTP \
  --target arn:aws:lambda:us-east-1:111122223333:function:hello-api

Output:

{
    "ApiId": "a1b2c3d4e5",
    "ApiEndpoint": "https://a1b2c3d4e5.execute-api.us-east-1.amazonaws.com",
    "Name": "hello-http-api",
    "ProtocolType": "HTTP"
}

You must also grant API Gateway permission to invoke the function (the Console does this automatically; the CLI --target shortcut usually does too, but if you build the route manually, add it):

aws lambda add-permission \
  --function-name hello-api \
  --statement-id apigw-invoke \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:us-east-1:111122223333:a1b2c3d4e5/*/*/hello"

Step 3 — test the live URL

curl "https://a1b2c3d4e5.execute-api.us-east-1.amazonaws.com/hello?name=Dev"

Output:

{"message":"Hello, Dev!"}

If you instead see {"message":"Internal Server Error"} with a 502, your function returned the wrong shape — check that you return statusCode and a string body (not a raw object).

Understanding the proxy event shape

With proxy integration, API Gateway hands your function the entire request as a single JSON event. For HTTP APIs this is payload format 2.0, which looks like:

{
  "version": "2.0",
  "routeKey": "GET /hello",
  "rawPath": "/hello",
  "queryStringParameters": { "name": "Dev" },
  "headers": { "content-type": "application/json" },
  "requestContext": { "http": { "method": "GET", "path": "/hello" } },
  "body": null,
  "isBase64Encoded": false
}

Read query strings from queryStringParameters, path/method from requestContext.http, and POST data from body (a string you JSON.parse). Note REST APIs use the older format 1.0, where the method lives at event.httpMethod — a common source of confusion when copying code between the two.

CORS — the browser gotcha

CORS (Cross-Origin Resource Sharing — the browser rule that controls which web pages may call your API) must be configured on the API, not just in your function code. A browser sends a preflight OPTIONS request before the real call; if API Gateway does not answer it with the right headers, the call fails with an opaque “CORS policy” error in the console — even though curl works fine.

Gotcha: Returning Access-Control-Allow-Origin from your Lambda alone is not enough. The browser’s preflight OPTIONS request often never reaches your function. Configure CORS at the API level so the gateway answers it.

Console: open the API → CORS → set Access-Control-Allow-Origin to your site (e.g. https://app.example.com), allow methods GET, POST, OPTIONS, and save.

CLI:

aws apigatewayv2 update-api \
  --api-id a1b2c3d4e5 \
  --cors-configuration AllowOrigins="https://app.example.com",AllowMethods="GET,POST,OPTIONS",AllowHeaders="content-type"

Best practices

  • Return the exact { statusCode, headers, body } shape; body must be a string, so always JSON.stringify JSON responses.
  • Prefer HTTP APIs over REST APIs for new projects — cheaper, faster, simpler — unless you need a REST-only feature.
  • Configure CORS at the API level for browser clients; never rely on Lambda headers for the preflight.
  • Use the $default stage with auto-deploy in dev, but create named stages (dev, prod) with stage variables for real environments.
  • Give the function a least-privilege execution role and set a sensible timeout (HTTP API integrations cap at 30 seconds).
  • Watch costs: HTTP APIs are about $1.00 per million requests plus Lambda’s per-request and per-GB-second charges — pennies for low traffic, but enable throttling to cap surprise bills.
Last updated June 15, 2026
Was this helpful?