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

Sponsored

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:

  1. Client sends HTTP request to the API Gateway endpoint (e.g., https://abcd1234.execute-api.us-west-2.amazonaws.com/prod/users).

  2. 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.

  3. API Gateway maps the request into a JSON event object. This includes headers, query string params, path parameters, body (base64 if binary), and stage variables.

  4. Lambda is invoked with that event and a context object. Your Python handler runs, reads data, executes business logic, and returns a response dict.

  5. 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 optionally isBase64Encoded.

  6. 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 body must be a string, not a dict. If you return a dict, Lambda will convert it to text but might break binary responses. Use json.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/hello restricts 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.yaml defines 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-permission step 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

  1. Use HTTP API instead of REST API for lower latency and cost, especially for simple proxy integrations.
  2. Adopt AWS SAM or Terraform to define Lambda and API Gateway as infrastructure as code, making deployments repeatable.
  3. 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_PROXY integration 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.