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:
- Open the Lambda console and click Create function.
- Choose Author from scratch, name it
hello-api, runtime Node.js 22.x, architecture arm64 (cheaper and faster). - 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:
- Open the API Gateway console and click Create API → HTTP API → Build.
- Under Integrations, choose Lambda and select your
hello-apifunction. Name the APIhello-http-api. - Click Next. Set the route to method GET, path
/hello. - Leave the stage as
$defaultwith 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-Originfrom your Lambda alone is not enough. The browser’s preflightOPTIONSrequest 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;bodymust be a string, so alwaysJSON.stringifyJSON 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
$defaultstage 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.