Build a Serverless API
Build a serverless API with API Gateway and Lambda — AWS Cloud & DevOps with Python.
Focus: build a serverless api with api gateway and lambda
You’ve crafted a fast, reliable Python API that runs beautifully on your laptop — but the moment you try to put it in production, reality hits: you need a server, a web framework, an SSL certificate, and someone to patch the OS at 2 AM. If that pain sounds familiar, this lesson is your escape hatch. Today you’ll learn how to build a serverless API with API Gateway and Lambda — a deployment model that eliminates server management, scales automatically, and costs pennies when idle. By the end, you’ll have a working HTTP endpoint backed by a Python function, ready to grow into a full production service.
The problem this lesson solves
Traditional APIs run on servers you own or rent. That means:
- Provisioning: You must launch EC2 instances, configure security groups, and install runtimes.
- Scaling: You either over-provision (wasting money) or under-provision (crashing under traffic spikes).
- Operations: Patching, monitoring, and handling server failures become your problem 24/7.
- Cost: You pay for idle capacity — servers running even when no requests come in.
For developers and DevOps engineers building Python apps, this overhead is a massive drain. You want to focus on business logic, not on restarting Tomcat or debugging nginx configs at midnight.
The serverless model flips this: you write a function, define an HTTP trigger, and AWS handles everything else. API Gateway becomes your front door (HTTP endpoints, routing, authentication), and Lambda runs your Python code on demand. No servers, no OS patching, no capacity planning. The cloud provider scales your API from one request to a million, then scales it back to zero when traffic drops.
This lesson is step 52 in your AWS Cloud & DevOps with Python path — the moment where you stop managing infrastructure and start shipping APIs.
Core concept / mental model
Think of a traditional restaurant: you own the building, hire the kitchen staff, and pay them even when the dining room is empty. That’s a server. Now think of a ghost kitchen: you send a dish to a central facility that cooks it only when an order comes in, charges you per dish, and handles health inspections for you. That’s serverless.
In AWS:
- Lambda is the kitchen — your Python function runs only when invoked, then the environment freezes or vanishes.
- API Gateway is the order window — customers hit a URL, API Gateway validates the request, routes it to Lambda, and returns the response.
The magic is in the connection: API Gateway and Lambda are deeply integrated. One POST to a URL triggers Python code, and you don’t manage any compute. You only see a function and a REST/HTTP API.
Here’s the mental model in words:
Client (browser, curl) -> API Gateway (HTTP endpoint) -> Lambda (Python function) -> (optional) DynamoDB/S3/etc.
| - method, path | - event, context |
| - auth, throttling | - returns JSON |
The event object contains everything about the HTTP request — method, headers, query params, body. The context object tells you about the runtime (function name, remaining time). Your function returns a dict that API Gateway formats into an HTTP response.
This is a compute service, not a web server. Lambda invokes your handler and hands it a well-defined input → output contract. Understanding that contract is the key to building robust serverless APIs.
How it works step by step
Let’s break down the flow from client to response:
-
Client sends HTTP request to the API Gateway endpoint (e.g.,
https://abcd1234.execute-api.us-west-2.amazonaws.com/prod/users). -
API Gateway receives the request and performs routing based on the method (
GET,POST) and path (/users). It may also authenticate (via API keys, Cognito, IAM) and apply throttling. -
API Gateway maps the request into a JSON
eventobject. This includes headers, query string params, path parameters, body (base64 if binary), and stage variables. -
Lambda is invoked with that event and a context object. Your Python handler runs, reads data, executes business logic, and returns a response dict.
-
Lambda sends the response back to API Gateway. If you’ve configured a proxy integration (default for
aws_proxy), Lambda must return a specific response shape:statusCode,headers,body(string/JSON), and optionallyisBase64Encoded. -
API Gateway formats and returns the HTTP response to the client.
That’s the whole lifecycle. It’s synchronous — the client waits for the Lambda function to complete. For long-running tasks (over 30 seconds), you’d need async patterns (like Step Functions), but for API basics, this is it.
Key point: The integration type matters. The aws_proxy (or LAMBDA_PROXY) integration is the simplest because it passes the entire event and expects a standard response. Avoid the older aws non-proxy integration unless you need custom mapping templates.
Hands-on walkthrough
Now let’s build a simple serverless “hello” API. You’ll need: an AWS account, the AWS CLI (with credentials), and Python 3.10+ or 3.11 installed. We’ll use the AWS CLI and zip for packaging (we’ll cover SAM/CloudFormation later).
Step 1: Write the Lambda function
Create a directory and a file called lambda_function.py:
# lambda_function.py
def lambda_handler(event, context):
"""A minimal Lambda handler for API Gateway."""
print("Received event:", event) # logs to CloudWatch
name = event.get("queryStringParameters", {}).get("name", "World")
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": '{"message": "Hello, ' + name + '!"}'
}
Pro tip: The
bodymust be a string, not a dict. If you return a dict, Lambda will convert it to text but might break binary responses. Usejson.dumps()for complex bodies.
Step 2: Package and upload
Lambda needs a deployment package. Zip your file:
# from the directory containing lambda_function.py
zip function.zip lambda_function.py
Then create the Lambda function with the AWS CLI:
aws lambda create-function \
--function-name hello-serverless \
--runtime python3.11 \
--role arn:aws:iam::123456789012:role/lambda-execution-role \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip
You need an execution role with basic Lambda permissions. If you don’t have one, create it:
aws iam create-role --role-name lambda-execution-role --assume-role-policy-document file://trust-policy.json
# trust-policy.json: {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}
aws iam attach-role-policy --role-name lambda-execution-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Step 3: Test the function directly
Before wiring API Gateway, verify it works standalone:
aws lambda invoke --function-name hello-serverless --payload '{"queryStringParameters": {"name":"Bob"}}' output.json
cat output.json
Expected output:
{"statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": "{\"message\": \"Hello, Bob!\"}"}
Step 4: Create the API Gateway REST API
Now expose it via HTTP. Use the console or CLI. For the CLI, here’s the high-level flow (abbreviated):
# Create REST API
REST_API_ID=$(aws apigateway create-rest-api --name 'HelloAPI' --query 'id' --output text)
# Get the root resource ID
ROOT_ID=$(aws apigateway get-resources --rest-api-id $REST_API_ID --query 'items[?path==`/`].id' --output text)
# Create a /hello resource
RESOURCE_ID=$(aws apigateway create-resource --rest-api-id $REST_API_ID --parent-id $ROOT_ID --path-part 'hello' --query 'id' --output text)
# Create a GET method
aws apigateway put-method --rest-api-id $REST_API_ID --resource-id $RESOURCE_ID --http-method GET --authorization-type NONE --no-api-key-required
# Integrate with Lambda (proxy)
aws apigateway put-integration \
--rest-api-id $REST_API_ID --resource-id $RESOURCE_ID --http-method GET \
--type AWS_PROXY \
--integration-http-method POST \
--uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:hello-serverless/invocations
# Deploy to a stage
aws apigateway create-deployment --rest-api-id $REST_API_ID --stage-name prod
# Give API Gateway permission to invoke Lambda
aws lambda add-permission \
--function-name hello-serverless \
--statement-id apigateway-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn arn:aws:execute-api:us-east-1:123456789012:$REST_API_ID/*/GET/hello
Note: The source ARN pattern
/*/GET/hellorestricts invocation to this specific method/path. Omitting it (using wildcard*) is easier but less secure.
Step 5: Call your API
Find the invoke URL in the console, or construct it:
curl https://$REST_API_ID.execute-api.us-east-1.amazonaws.com/prod/hello?name=Alice
Expected output:
{"message": "Hello, Alice!"}
You just built a serverless API! It scales automatically and you pay only when a request runs.
Compare options / when to choose what
There’s more than one way to expose Lambda via HTTP. Here’s a comparison:
| Option | Integration type | Best for | Drawbacks |
|---|---|---|---|
| REST API + Lambda proxy | AWS_PROXY |
Simple CRUD, full control over request/response | You must handle CORS, validation manually |
| HTTP API | AWS_PROXY (or non-proxy) |
Lower latency, cheaper, simpler | Fewer advanced features (no custom domain mapping built-in, limited auth) |
| REST API with mapping templates | AWS (non-proxy) |
Filtering/transforming payloads before Lambda | Complex; you write VTL templates |
| API Gateway + Step Functions | AWS_PROXY (to Step Functions) |
Long-running workflows, orchestration | Adds latency, more moving parts |
For most Python developers, HTTP API is the sweet spot: it’s up to 70% cheaper at scale, has lower latency, and supports the same proxy integration. Choose REST API only when you need features like API keys, usage plans, or WAF integration. We used REST API above because it demonstrates the concepts and is widely used, but consider switching.
Variations
- Use the AWS Console for prototyping — it’s clicks, not CLI, but easier for beginners. You can create the Lambda function, then “Add trigger” > API Gateway.
- Use SAM (Serverless Application Model) for production infrastructure as code. A
template.yamldefines both Lambda and API Gateway in one file — we’ll cover this in the next lessons. - Use Chalice or Zappa — Python frameworks that abstract API Gateway entirely. Chalice, from AWS, lets you write
@app.route("/hello")and deploy with one command. Great for simplicity, but you’re locked into the framework.
Troubleshooting & edge cases
Here are the most common pitfalls when you build a serverless API:
502 Bad Gateway
This usually means Lambda returned an invalid response. For proxy integration, your response must be a dict with statusCode and body as strings. If you return a Python dict as the body (not JSON stringified), you’ll get 502. Fix:
import json
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"message": "Hello"})
}
403 Forbidden from API Gateway
You haven’t allowed API Gateway to invoke Lambda. Check the aws lambda add-permission step. Also confirm the source ARN matches your API ID and stage (e.g., /*/GET/hello).
CORS errors
Your client-side JavaScript can’t call the API because you didn’t add CORS headers. In the console, enable CORS on the resource, or return headers in Lambda:
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST"
}
For preflight OPTIONS requests, you might need a separate method that returns 200 with CORS headers — or use a mock integration. This is a classic pain point, and many hours have been lost to invisible CORS errors.
Cold starts
First invocation after idle time takes longer (e.g., 2–3 seconds instead of 50 ms). It’s normal. Mitigate by using provisioned concurrency, but that costs money. For latency-sensitive APIs, keep Lambda warm or switch to containers (ECS Fargate).
Timeouts and async tasks
Lambda is limited to 15 minutes max, but API Gateway times out after 29 seconds. If your function takes longer, you need an async pattern: fire Lambda, return immediately, then client polls a status. Use Step Functions or SQS.
Event structure differences
REST APIs and HTTP APIs pass different event formats. For example, in HTTP API, the path params are under event["pathParameters"] (different casing). Always log the event and inspect it — don’t assume.
isBase64Encoded for binaries
If you return binary data (e.g., images), set isBase64Encoded: true and configure API Gateway to enable binary support. Otherwise, clients get corrupted data.
What you learned & what's next
You now know how to build a serverless API with API Gateway and Lambda in Python. You can:
- Explain the problem of traditional servers and how serverless solves it.
- Use Lambda’s event/context contract and return proper proxy responses.
- Wire API Gateway to Lambda via the CLI, including permissions and deployment.
- Troubleshoot common issues like 502s, CORS, and cold starts.
This solid foundation prepares you for the next lessons: you'll learn to secure your API with API keys and Cognito, automate deployment using SAM or CloudFormation, and add persistent storage with DynamoDB. A serverless API is only the beginning — combining it with infrastructure as code and managed services will make you a truly cloud-native DevOps engineer.
Before you move on, try this: add a POST method to your API that reads event["body"], parses JSON, and returns a personalized response. You’ll practice the same pattern, and you’ll be ready for the next lesson on building a full CRUD API with DynamoDB.
Practice recap
Extend your current hello-serverless function: add a POST method that reads the JSON body (use the json module), validates a required field, and returns a 400 error if missing. Then redeploy and test with curl -X POST. This reinforces the request/response contract and prepares you for handling real-world input.
Common mistakes
- Returning a Python dict directly as the body — API Gateway proxy expects a string. Always use
json.dumps(). - Forgetting the
aws lambda add-permissionstep for API Gateway, causing 403 Forbidden errors. - Ignoring CORS headers — client-side apps fail with silent CORS errors in the browser.
- Assuming Lambda’s 15-minute timeout — API Gateway caps at 29 seconds, breaking long requests.
Variations
- Use HTTP API instead of REST API for lower latency and cost, especially for simple proxy integrations.
- Adopt AWS SAM or Terraform to define Lambda and API Gateway as infrastructure as code, making deployments repeatable.
- Try the Chalice or Zappa Python frameworks to bypass manual API Gateway setup and write Flask-like code.
Real-world use cases
- A mobile app's public REST API for fetching user profiles — low traffic, zero idle cost.
- A webhook endpoint that receives events from third parties and triggers data processing pipelines.
- An internal admin API for uploading files to S3 via presigned URLs, authenticated by API keys.
Key takeaways
- Lambda is a compute service that runs Python code on demand; API Gateway is the HTTP front door.
- Use
AWS_PROXYintegration for simplicity — the event object maps directly to the request and the response dict dictates HTTP status. - Package your function as a zip with dependencies included; test it standalone before wiring API Gateway.
- Always configure the Lambda permission for API Gateway and handle CORS for browser clients.
- Watch out for the 29-second API Gateway timeout and the 15-minute Lambda limit for async jobs.
- REST APIs offer the most features, but HTTP APIs are cheaper and faster for basic use.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.