Blog
February 8, 2017 Marie H.

Building a REST API with AWS Lambda and API Gateway

Building a REST API with AWS Lambda and API Gateway

Photo by <a href="https://unsplash.com/@comparefibre?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Compare Fibre</a> on <a href="https://unsplash.com/?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Unsplash</a>

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.

  1. Create a resource — this is your URL path. Let's say /hello.
  2. On that resource, create a method. Pick GET (or ANY if you want all methods).
  3. For integration type, choose Lambda Function and check Use Lambda Proxy Integration. This is the checkbox that makes the event dict work the way shown above.
  4. 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.