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

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:
- HTTP Methods: Supports GET, POST, PUT, DELETE, etc.
- API Keys: Restricts access at scale, combined with usage plans for throttling.
- CORS: Configured via integration responses, simplifying browser interactions.
- 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:
- Service Decoupling: Each microservice can evolve independently.
- Scalability and Performance: Serverless functions scale horizontally, handling spikes without manual intervention.
- Multi-Platform Support: Serve clients ranging from web and mobile apps to IoT devices and third-party integrations.
- Security and Access Control: Layer in authentication, usage throttling, and fine-grained authorization.
- 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.