Needed to throw together a quick API endpoint without standing up a server and went down the Lambda + API Gateway rabbit hole. The AWS docs for this are... fine, but they bury the important parts. Here's the version I wish I'd found first.
The idea is simple: API Gateway receives an HTTP request, invokes your Lambda function with the request details, and returns whatever your function sends back. No EC2, no load balancer, no nginx config files at 11pm.
Creating the Lambda function
Go to Lambda in the console, hit Create Function, pick "Author from scratch". Name it something sensible, pick Python 3.6, and create or select an execution role with basic Lambda permissions.
The handler is the key part. API Gateway with proxy integration passes the entire HTTP request as an event dict. Your function needs to return a specific dict shape or API Gateway will throw a 502:
import json
def handler(event, context):
# event contains everything about the HTTP request
http_method = event.get('httpMethod')
path = event.get('path')
query_params = event.get('queryStringParameters') or {}
body = event.get('body')
# Parse JSON body if present
if body:
try:
body = json.loads(body)
except ValueError:
pass
# Do your actual work here
response_body = {
'message': 'Hello from Lambda',
'method': http_method,
'path': path,
'query': query_params
}
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json'
},
'body': json.dumps(response_body)
}
The return dict must have statusCode and body. The body must be a string, hence the json.dumps. Forget this and spend 20 minutes wondering why you're getting 502s — ask me how I know.
Setting up API Gateway
In the API Gateway console, create a new API (REST API, not the "HTTP API" option). Give it a name.
- Create a resource — this is your URL path. Let's say
/hello. - On that resource, create a method. Pick GET (or ANY if you want all methods).
- For integration type, choose Lambda Function and check Use Lambda Proxy Integration. This is the checkbox that makes the
eventdict work the way shown above. - Enter your function name and save. It'll ask to add permission — say yes.
Deploying a stage
The API doesn't have a URL until you deploy it. Click Actions → Deploy API, create a new stage called prod (or dev, whatever). You'll get a URL like:
https://abc123def4.execute-api.us-east-1.amazonaws.com/prod
Your endpoint is at <base_url>/hello.
Testing with curl
$ curl -s https://abc123def4.execute-api.us-east-1.amazonaws.com/prod/hello | python -m json.tool
{
"message": "Hello from Lambda",
"method": "GET",
"path": "/hello",
"query": {}
}
With query parameters:
$ curl -s "https://abc123def4.execute-api.us-east-1.amazonaws.com/prod/hello?name=marie&env=prod" | python -m json.tool
{
"message": "Hello from Lambda",
"method": "GET",
"path": "/hello",
"query": {
"env": "prod",
"name": "marie"
}
}
POST with a body
For POST requests the body comes through as a string in event['body']. The handler above already handles that. Test it:
$ curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"action": "do_something"}' \
https://abc123def4.execute-api.us-east-1.amazonaws.com/prod/hello | python -m json.tool
Error handling
Return the appropriate HTTP status in statusCode. API Gateway passes it through faithfully:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'bad request', 'detail': 'missing required field'})
}
A note on cold starts
Lambda functions that haven't been invoked recently take a second or two to start up ("cold start"). For a low-traffic internal API this is usually fine. If you need consistent response times you'll want to look at keeping functions warm with scheduled pings, but that's a whole other post.
Total infrastructure cost for a low-traffic API like this is basically zero — Lambda's free tier covers 1 million invocations per month. Hard to argue with that.