Building Serverless APIs with API Gateway and AI

Explore a serverless API using AI-enhanced workflows with AWS Lambda, API Gateway, and CloudFormation.

Photo Credit: https://unsplash.com/@jakefagan

Over the past several months, I've been using AI-assisted IDEs like Windsurf to build a variety of projects. I’ve even documented my Windsurf rules here. A recurring need is to expose these apps through APIs, which is where Amazon API Gateway comes in.

From leading engineering teams using serverless, I've seen how AWS Lambda and API Gateway cut costs and minimize overhead. Configuring these services by hand can be tricky, though. Generative AI makes it easier, but if left unguided, it can become unwieldy due to quirks in both the AI and the services themselves.

To help you (and my future self), I’ve created a "Hello World" API example. Below, you’ll find code, deployment steps, and a concise overview of how everything fits together.

Understanding the Solution Architecture

At a high level, we’re leveraging several AWS services that work together to provide a fully managed, secure, and highly scalable REST API:

  • AWS Lambda: Executes our Python code in response to API requests.
  • Amazon API Gateway: Routes incoming requests to the Lambda function, handles authentication, and provides a REST interface.
  • AWS Parameter Store: Stores configuration parameters so you can keep secrets and environment-specific settings outside of your code.
  • AWS CloudFormation: Defines, manages, and provisions the infrastructure as code.
  • AWS IAM: Controls access to your resources through roles and permissions.

This approach embodies essential serverless tenets: it’s stateless, scales automatically, and you pay only when your code is running. Most importantly, you can focus on innovation and iteration and less on infrastructure management.

Step-by-Step Implementation Guide

Below is an example workflow for setting up and deploying this serverless API. All commands assume you’re using Python 3.11 (or later) and have the AWS CLI properly configured.

git clone https://github.com/PaulDuvall/public.git
cd public/api_gateway

1. Setting Up Your Environment

Clone the repository containing your “Hello World” Lambda and run the setup:

chmod +x run.sh
./run.sh setup

This script creates a Python virtual environment and installs dependencies from requirements.txt. Make sure to verify that your AWS credentials are correctly configured so CloudFormation can deploy resources in your account.

2. Testing Your Lambda Function Locally

Before deploying, confirm your Lambda function’s behavior using local tests:

./run.sh test

This runs a comprehensive suite of tests validating the function’s behavior across GET and POST methods, as well as verifying error handling and CORS responses.

3. Deploying the Complete Solution

To stand up the entire stack in AWS, run:

./run.sh all

This script bundles packaging, testing, and CloudFormation-based deployment into a single command. You can skip the setup and test commands and just run all if you choose. CloudFormation provisions:

  • A Lambda function (lambda_function.py)
  • An API Gateway configuration – including GET and POST calls
  • IAM roles and permissions
  • Parameter Store entries

It can take about 5-10 minutes to finish the execution of this script. You may need to quit when it outputs configuration in order for the script to continue.

4. Securing Your API

You don't need to run this manually, as ./run.sh all handles the test API calls—but you can still retrieve the generated API key for secure access:

./manage_api_key.sh get-api-key HelloWorldApiStack

Calls to your API must include this key in the x-api-key header. The Lambda function is also designed to handle production-safe error messages, ensuring sensitive details aren’t exposed to clients.

5. Testing Your Deployed API

Although ./run.sh all automatically generates and tests GET and POST requests, you can also run them manually to validate the deployment.

curl -H "x-api-key: YOUR_API_KEY" "https://your-api-id.execute-api.region.amazonaws.com/prod/hello?name=YourName"

# Test with POST
curl -X POST \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{"name":"PostUser","message":"This is a POST request!"}' \
  "https://your-api-id.execute-api.region.amazonaws.com/prod/hello"

You should see a valid JSON response in both cases, confirming that your API is fully functional.

How This Approach Fits Into Your Cloud Strategy

CloudFormation: Infrastructure as Code

Defining and deploying everything via AWS CloudFormation ensures consistency and repeatability. You can track every infrastructure change in version control, enforce code reviews on stack modifications, and reliably replicate your infrastructure across multiple environments.

Lambda: Serverless Compute

The Python-based Lambda function (e.g., lambda_function.py) keeps your application code separate from infrastructure concerns. With built-in concurrency scaling, you’ll handle surges in traffic without any extra manual steps. Coupled with environment variables stored in AWS Systems Manager Parameter Store, each deployment stage (dev, test, prod) can load unique configuration values.

Below is a simplified example of the Lambda function:

# Filename: lambda_function.py

import json
import os
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    """Handle GET and POST requests with optional name & message."""
    try:
        # Extract method
        method = event.get("httpMethod", "GET")

        # Extract query params for GET
        query_params = event.get("queryStringParameters", {}) or {}
        name = query_params.get("name", "World")

        # Extract body for POST
        if method == "POST":
            body = json.loads(event.get("body", "{}"))
            name = body.get("name", name)
            message = body.get("message", "Hello from POST!")
        else:
            message = f"Hello from GET, {name}!"

        # Basic response
        response = {
            "statusCode": 200,
            "headers": {
                "Content-Type": "application/json",
                "Access-Control-Allow-Origin": "*",  # CORS support
            },
            "body": json.dumps({"name": name, "message": message})
        }
        return response

    except Exception as exc:
        logger.error(f"Error processing request: {exc}")
        return {
            "statusCode": 500,
            "headers": {
                "Content-Type": "application/json",
                "Access-Control-Allow-Origin": "*",
            },
            "body": json.dumps({"error": "Internal Server Error"})
        }

This code follows AWS best practices: environment-based configuration, robust exception handling, and clarity through a single-responsibility function.

API Gateway: RESTful Interface

Amazon API Gateway handles routing, transformation, and security with features like:

  1. HTTP Methods: Supports GET, POST, PUT, DELETE, etc.
  2. API Keys: Restricts access at scale, combined with usage plans for throttling.
  3. CORS: Configured via integration responses, simplifying browser interactions.
  4. Automatic Metrics: Logs calls to Amazon CloudWatch for visibility into performance and usage.

Parameter Store: Centralized Configuration

Storing environment-specific parameters in Parameter Store adheres to the Twelve-Factor App principle of externalizing configuration. It keeps secrets secure (via encryption) and lets you change values without redeploying code.

Why Expose Functionality Through APIs?

In modern microservice and serverless architectures, APIs act as contracts between components. This approach ensures:

  1. Service Decoupling: Each microservice can evolve independently.
  2. Scalability and Performance: Serverless functions scale horizontally, handling spikes without manual intervention.
  3. Multi-Platform Support: Serve clients ranging from web and mobile apps to IoT devices and third-party integrations.
  4. Security and Access Control: Layer in authentication, usage throttling, and fine-grained authorization.
  5. Analytics and Monitoring: Track usage patterns and errors to continuously improve.

Best Practices and Common Pitfalls

Lambda Function Design

  • Single Responsibility: Keep each function narrowly focused.
  • Use Environment Variables: Never hardcode secrets or environment specifics.
  • Minimize Cold Starts: Keep package size small; only import necessary modules.

API Gateway Configuration

  • Resource Policies: Enforce IP restrictions or VPC-based access when needed.
  • CloudWatch Logs: Monitor traffic and errors in real time.
  • Validate Input: Implement request validation models to avoid malformed data.
  • Response Models: Document your schema so consumers know what to expect.

Common Pitfalls

  • Incorrect IAM Permissions: A misconfigured role can block Lambda from reading Parameter Store or returning responses.
  • Missing Authentication: Always use an API key or a more advanced mechanism (like Cognito or OAuth) for production.
  • CORS Misconfigurations: Make sure CORS headers are returned even on errors.
  • Unescaped Query Parameters: Properly encode special characters to avoid confusion in shell-based tests.

Extending the Solution

This “Hello World” example is just a start. You can evolve it by:

  • Adding Database Integration: Use Amazon DynamoDB or Amazon RDS for persistent storage.
  • Implementing OAuth: Integrate with an OpenID provider for fine-grained user authentication.
  • Custom Authorizers: Build Lambda-based logic to control access based on tokens or roles.
  • Distributed Tracing: Instrument your code with AWS X-Ray for end-to-end observability.
  • Using Frameworks: Accelerate development and streamline deployment using AWS SAM or AWS CDK—both defining infrastructure as code.

By combining AWS Lambda, API Gateway, and CloudFormation, you can deploy highly scalable and cost-efficient APIs without managing servers. This streamlined approach is a real-world application of the twelve-factor methodology and modern DevOps practices—simplifying operational overhead while maintaining security, observability, and reliability.

Remember, “serverless” doesn’t mean you can ignore operations—it means AWS handles much of the heavy lifting. You still need robust logging, monitoring, and diagnostics to ensure your API runs smoothly in production. Thankfully, with the patterns and tooling introduced in this post, you’ll be well-equipped to tackle those challenges.

Additional References