Build a Serverless API on AWS

Deploy a Python-powered REST API using AWS Lambda and API Gateway.

Introduction

30 Second Summary

Every time you check the weather on your phone, search for a restaurant, or get a notification, an API is working behind the scenes to deliver that data. APIs are the invisible backbone connecting the apps you use every day to the servers that power them.

In this project, you will build your own serverless API on AWS that serves random fun facts about technology and programming. You'll end up with a live URL that anyone on the internet can visit to get a JSON response from your Python-powered cloud function.

What You'll Build

Paste your API's URL into any browser and hit Enter. A random fun fact appears on screen, served by your own Python function running in the cloud with no servers to manage.

By the end of this project, you'll have:

  • A live URL you can paste into any browser to see a random fun fact, powered by a Python function running on AWS Lambda with zero server management.
  • A public REST API endpoint built with API Gateway that anyone can visit from a browser or test from a terminal.
  • Hands-on experience debugging a 502 Bad Gateway error, one of the most common real-world mistakes when connecting Lambda to API Gateway.
  • Secret Mission: Add query parameter filtering so users can request facts by category like ?category=programming.

Are there any prerequisites?

You need basic Python knowledge and an AWS account. If you do not have an AWS account yet, Step 1 walks you through creating one for free.

Before We Start

Before diving in, take a moment to lock in what you are about to build and why it matters for your goals.

Set Up Your AWS Account and Create Your First Lambda Function

Your Fun Facts API needs somewhere to run. Traditionally, that means provisioning a server, installing software, and keeping everything patched. AWS Lambda eliminates all of that by running your code on demand with zero server management.

In this step, you will sign into the AWS console and create your first Lambda function. The moment you test it, you will see your Python code execute in the cloud without a single server to set up.

In this step, get ready to:
  • Sign into the AWS Management Console.
  • Create a Lambda function named funFactsAPI.
  • Test your function and see a successful response.
Sign into the AWS Management Console

Everything you build in this project lives in AWS. Your first task is to confirm you can sign into the console.

✔️ I'm signed into the console

You should see the AWS Management Console dashboard. You are ready to create your first Lambda function.

ⓧ I don't have an AWS account yet

Will this cost me anything?

Lambda is Always Free for up to 1,000,000 requests per month. API Gateway is free for 1,000,000 calls per month for the first 12 months. This project stays well within those limits, so you will not be charged.

Create your funFactsAPI Lambda function

Now you will create a serverless function in Lambda. This function will eventually serve fun facts, but for now it will return a simple welcome message to prove everything works.

  • Type Lambda in the search bar at the top of the AWS console.
  • Select Lambda from the search results to open the Lambda console.
  • Click Create function.
  • Select Author from scratch.
  • Enter funFactsAPI in the Function name field.
  • Select Python 3.14 from the Runtime dropdown.
  • Leave the permissions at their default. Lambda automatically creates an execution role for your function.
  • Click Create function at the bottom of the page.

After a moment, the Lambda console loads your new function's page. You should see a success banner confirming the function was created.

What is an execution role?

Lambda needs permission to run in your AWS account. The execution role is an IAM role that grants your function basic permissions, like writing logs. AWS creates one for you automatically when you leave the defaults.

Lambda created a default "Hello world" function for you. You will now replace that code with your own welcome message handler.

  • Make sure you are on the Code tab.
  • Click lambda_function.py in the file explorer if it is not already open.
  • Select all the existing code and delete it.
  • Replace it with the following code:
import json

def lambda_handler(event, context):
    # Return a JSON response with a welcome message
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json"
        },
        "body": json.dumps({"message": "Welcome to the Fun Facts API!"})
    }

What does this code do?

`lambda_handler` is the function Lambda calls every time your function is triggered. It receives two arguments: `event` (the incoming request data) and `context` (runtime information about the invocation).

The function returns a dictionary with a `statusCode` of `200` (success), a `Content-Type` header, and a `body` containing a JSON-encoded welcome message. The `json.dumps()` call converts the Python dictionary into a JSON string.

  • In the DEPLOY section, click Deploy to save your code to Lambda.

You should see a banner confirming the function was updated successfully.

Not seeing the success banner?

  • Check for a red error banner at the top of the editor. This usually means a Python syntax error. Make sure every line is indented with spaces (not tabs) and all quotes are matched.
  • Make sure you clicked Deploy in the DEPLOY section, not a different button.

Help me fix my Lambda deploy error.

✔️ Awesome, I've got everything!

Great. Make sure you clicked Deploy and saw the success banner before continuing.

ⓧ I'd like to double check the full code

Here is the complete lambda_function.py file at this point. Make sure your code matches exactly:

import json

def lambda_handler(event, context):
    # Return a JSON response with a welcome message
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json"
        },
        "body": json.dumps({"message": "Welcome to the Fun Facts API!"})
    }
Test your function

Your code is deployed, but you have not run it yet. Lambda lets you send a test event to your function directly in the console so you can verify it works before connecting it to anything else.

  • Click the Test tab at the top of your function page.
  • Under Test event, select Create new event.
  • Enter testEvent in the Event name field.
  • Leave the default event JSON as-is. Your handler does not use the event data for now.

Before you click Test, take a moment to predict: what do you think the response will contain? Will it succeed or fail?

  • Click Test.

Under Execution result, expand Details. You should see a response like this:

{
  "statusCode": 200,
  "headers": {
    "Content-Type": "application/json"
  },
  "body": "{\"message\": \"Welcome to the Fun Facts API!\"}"
}

What should I see?

The green "Executing function: succeeded" banner confirms your function ran without errors. The response shows `statusCode` 200 and your welcome message in the body.

Notice the execution duration in the summary. Lambda ran your code in just a few milliseconds. That is the power of serverless. No boot-up time, no server provisioning. Your code just runs.

Seeing an error instead?

  • If you see a "Task timed out" error, check that your function code does not have an infinite loop.
  • If you see a syntax error, go back to the Code tab. Make sure your code matches the code block above exactly, then click Deploy and try the test again.
  • If the response body looks empty or malformed, check that json.dumps() wraps the dictionary in the body field.

Help me debug my Lambda test error.

Your Lambda function is live and responding. Next up, you will replace the hardcoded welcome message with a dynamic fun facts engine that returns a different fact every time.

Build the Fun Facts Engine

Your Lambda function is live and responding with a hardcoded welcome message. That proves the plumbing works, but a static response is not much of an API.

In this step, you'll load your function with a collection of fun facts about technology and programming. Then you'll write logic that picks a different fact at random every time someone calls the function.

In this step, get ready to:
  • Add a library of fun facts with categories to your Lambda function.
  • Write a handler that picks a random fact and formats a proper JSON response.
  • Deploy and test to confirm a different fact appears each time.
Add the imports and fun facts data

Every API needs data to serve. You'll replace the existing hardcoded handler with a proper data set and the imports your function needs to pick a random fact and stamp each response with a timestamp.

  • In the Lambda console, make sure you're on the Code tab for your funFactsAPI function.
  • In the Code source pane, click lambda_function.py to open it in the editor.
  • Select all the existing code and delete it.
  • Paste the following imports and facts list into the empty editor:
import json
import random
from datetime import datetime, timezone

# A list of fun facts, each with a text and a category
FACTS = [
    {"text": "The first computer bug was an actual moth found in a Harvard Mark II computer in 1947.", "category": "history"},
    {"text": "Python was named after Monty Python's Flying Circus, not the snake.", "category": "programming"},
    {"text": "The first 1 GB hard drive, introduced in 1980, weighed about 550 pounds.", "category": "hardware"},
    {"text": "The average smartphone today has more computing power than NASA used to send astronauts to the moon.", "category": "hardware"},
    {"text": "JavaScript was created in just 10 days by Brendan Eich in 1995.", "category": "programming"},
    {"text": "The first website ever created is still online at info.cern.ch.", "category": "history"},
    {"text": "About 90% of the world's currency exists only in digital form.", "category": "history"},
    {"text": "The Python programming language uses indentation to define code blocks instead of curly braces.", "category": "programming"},
    {"text": "A single Google search uses about 0.3 watts of energy.", "category": "hardware"},
    {"text": "Git was created by Linus Torvalds in 2005 to manage Linux kernel development.", "category": "programming"}
]

What does this code do?

The first three lines import Python standard library modules. json converts Python dictionaries into JSON strings. random picks a random item from a list. datetime and timezone let you stamp each response with the current time in UTC.

The FACTS list holds 10 dictionaries, each with a "text" key for the fact itself and a "category" key that tags it as "history", "programming", or "hardware". These categories will come in handy later.

Build the response handler

With the data in place, you need a handler function that picks a random fact and packages it into the response format that API Gateway expects.

  • Directly below the FACTS list (after the closing ]), paste the following handler function:
def lambda_handler(event, context):
    # Use the full facts list
    all_facts = FACTS

    # Pick one fact at random
    fact = random.choice(all_facts)

    # Build the response payload
    body = {
        "fact": fact["text"],
        "category": fact["category"],
        "timestamp": datetime.now(timezone.utc).isoformat()
    }

    # Return in API Gateway proxy response format
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*"
        },
        "body": json.dumps(body)
    }

What does this code do?

random.choice(all_facts) picks one fact dictionary at random every time the function runs. The body dictionary packages the fact's text, its category, and a UTC timestamp into a clean payload.

The return statement uses the API Gateway proxy response format: an integer statusCode, a headers dictionary, and a body that is a JSON-encoded string (via json.dumps()). This format is critical. API Gateway will reject the response if it does not follow this structure.

The Access-Control-Allow-Origin: "*" header is a CORS header. It tells browsers that any website is allowed to call your API. Without it, browser-based apps would be blocked from reaching your endpoint.

  • Click Deploy to save your updated function code.

Seeing an error when you deploy?

  • Check for a missing comma between items in the FACTS list. Every dictionary in the list needs a comma after it except the last one.
  • Make sure def lambda_handler(event, context): is at the top level (no indentation). Everything inside the function should be indented by four spaces.
  • Confirm you deleted all the old code before pasting. Leftover lines from the previous handler can cause syntax errors.
  • Ask for help debugging your deploy error.

✔️ Awesome, I've got everything!

Great. Make sure you have clicked Deploy and saved your changes before moving on.

ⓧ I'd like to double check the full code

Your lambda_function.py should have two sections in this order. Scroll up and confirm each one is present in your Lambda editor:

  • Imports and FACTS list (first chunk): three import lines at the top, followed by the FACTS list containing 10 dictionaries with "text" and "category" keys.
  • Handler function (second chunk): def lambda_handler(event, context): that uses random.choice() to pick a fact, builds a body dictionary with fact, category, and timestamp keys, and returns the API Gateway proxy response format with statusCode, headers, and a json.dumps(body) string as the body.
Test your updated function

Your function has new data and new logic. Before you click Test, think about what you expect the response to look like this time compared to the static welcome message you saw in Step 1.

  • Click the Test tab.
  • Select your existing test event, then click Test.

Under Execution result, expand Details. You should see a 200 status code and a response body containing a random fun fact, a category, and a timestamp.

  • Click Test two or three more times.

Each time you click Test, the fact in the response should change. The category and timestamp update too. That randomness is your function processing real logic on every invocation.

Seeing the old welcome message?

  • Make sure you clicked Deploy before clicking Test. The console runs whatever version was last deployed, not what is currently in the editor.
  • If you see a null response or an error, check that you did not accidentally leave the old lambda_handler function in the file alongside the new one. There should be only one def lambda_handler definition.
  • Ask for help troubleshooting your test result.

Your function now returns dynamic data. Every invocation is a fresh roll of the dice. Next up, you'll connect this function to API Gateway so anyone on the internet can trigger it with a URL.

Connect API Gateway and Hit Your First Error

Your Lambda function works perfectly inside the AWS console. Click Test, see a random fun fact. But here is the problem. Nobody else on the internet can reach your function. There is no URL, no endpoint, and no way for the outside world to send it a request.

To turn your function into a real API that anyone can call, you need Amazon API Gateway. API Gateway creates a public URL that forwards incoming requests to your Lambda function and sends the response back to the caller. In this step, you will create that connection and test it.

In this step, get ready to:
  • Create a REST API named FunFactsAPI in the API Gateway console.
  • Connect your funFactsAPI Lambda function to a GET method using Lambda proxy integration.
  • Test the integration and examine how Lambda responds through API Gateway.
Create your REST API in API Gateway

Right now, your Lambda function lives inside the AWS cloud with no front door. API Gateway is that front door. It receives HTTP requests from the internet and routes them to your Lambda function.

  • In the AWS Management Console, type API Gateway in the search bar at the top.
  • Select API Gateway from the results.
  • Click Create API.
  • Find the REST API section (not REST API Private) and click Build.
  • For API name, enter FunFactsAPI.
  • Keep API endpoint type set to Regional.
  • Click Create API.

You should see your new FunFactsAPI with a Resources pane showing the root resource /.

What is API Gateway?

API Gateway is an AWS service that acts as a "front door" for your backend services. It receives HTTP requests from the internet, routes them to the right backend (like a Lambda function), and returns the response.

Without API Gateway, your Lambda function can only be invoked from inside the AWS console. API Gateway gives it a public URL that anyone with a browser or curl can call.

Now you need to tell API Gateway what to do when someone sends a GET request to the root URL. You will create a GET method that forwards requests to your Lambda function.

  • Select the / resource in the Resources pane.
  • Click Create method.
  • For Method type, select GET.
  • For Integration type, select Lambda function.
  • Turn on Lambda proxy integration.
  • For Lambda function, type funFactsAPI and select it from the dropdown.
  • Click Create method.

You should now see the GET method listed under the / resource in the Resources pane.

What is Lambda proxy integration?

When you turn on Lambda proxy integration, API Gateway passes the entire incoming HTTP request directly to your Lambda function as the event parameter. This includes headers, query strings, the request body, and path information.

In return, API Gateway expects your Lambda function to send back a response in a specific format with three fields: statusCode, headers, and body. If the format does not match, API Gateway cannot process the response and returns an error.

Don't see your Lambda function in the dropdown?

Make sure you are in the same AWS Region where you created your Lambda function. Check the region selector in the top-right corner of the console.

Help me find my Lambda function in API Gateway.

Test the API connection

API Gateway has a built-in Test feature that lets you invoke your method without deploying the API. This is useful for checking that the connection to Lambda works before making it public.

Before you run the test, take a moment to predict. Do you think your Lambda function will respond correctly through API Gateway on the first try, or will something go wrong?

  • Click the GET method under the / resource.
  • Click the Test tab. You may need to click the right arrow button to reveal it.
  • Leave all fields empty (no query strings, headers, or request body needed).
  • Click the Test button.

The test result panel shows several pieces of information. Look at the Status field. You should see 200. Under Response body, you should see your fun fact JSON with fact, category, and timestamp fields.

What does the test result show?

The test result panel displays five sections. Request shows the resource path that was called. Status shows the HTTP status code from your Lambda function.

Response body shows the data your function returned. Response headers shows the headers like Content-Type. Logs shows simulated CloudWatch log entries for the invocation.

Seeing a 500 error or no response?

Make sure you turned on Lambda proxy integration when creating the method. Without it, API Gateway expects a different response format from Lambda.

Also confirm your Lambda function is deployed. Return to the Lambda console and check that your latest code changes are saved and deployed.

Help me fix my API Gateway test error.

Understand the proxy response format

Your test succeeded because your Lambda function already returns the exact format API Gateway expects. This format is the most common source of errors when building serverless APIs, so understanding it now will save you debugging time in every future project.

What format does API Gateway require?

With Lambda proxy integration, API Gateway expects your function to return a dictionary with exactly three fields.

  • statusCode must be an integer like 200 or 400. This becomes the HTTP status code in the response.
  • headers must be a dictionary of response headers like Content-Type and CORS headers.
  • body must be a JSON-encoded string, created with json.dumps(). API Gateway does not accept a raw Python dictionary here.

What happens without the right format?

If your Lambda function returns a plain Python dictionary without the statusCode, headers, and string body fields, API Gateway cannot process it. You will see a 502 Bad Gateway error instead of your data.

This is one of the most common mistakes developers make when connecting Lambda to API Gateway for the first time. The fix is always the same: make sure your function returns statusCode as an integer, headers as a dictionary, and body as a json.dumps() string.

Your API Gateway is connected to Lambda and working. But right now, only you can test it through the console. In the next step, you will deploy your API to get a public URL that anyone on the internet can visit.

Fix the Response and Go Live

In the last step, you saw your Lambda function and API Gateway struggle to communicate. The 502 error revealed a critical lesson: API Gateway expects a very specific response format from Lambda, and anything else gets rejected.

Now you will verify that your function's response format is correct, deploy your API to a live stage, and test it from the open internet. By the end of this step, you will have a real URL you can share with anyone.

In this step, get ready to:
  • Verify your Lambda function returns the correct API Gateway proxy response format.
  • Deploy your API to a production stage and get a public URL.
  • Test your live API from a browser and PowerShell.
Verify your Lambda function's response format

API Gateway's Lambda proxy integration requires a specific response shape. If any piece is wrong or missing, you get a 502. Time to confirm your function has it right.

  • Switch back to the Lambda console tab in your browser.
  • Click on your funFactsAPI function to open it.
  • Scroll down to the code editor and find the return statement inside lambda_handler.

Your return statement should look like this:

    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*"
        },
        "body": json.dumps(body)
    }

What makes this format correct?

API Gateway's proxy integration requires exactly three keys in the return dictionary.

  • statusCode must be an integer (200), not a string.
  • headers must be a dictionary. Content-Type tells the browser the response is JSON. Access-Control-Allow-Origin is a CORS header that lets any website call your API.
  • body must be a JSON-encoded string created with json.dumps(). Returning a raw Python dictionary here is the most common cause of 502 errors.
  • Confirm your code matches the format above. If anything is different, update your code in the Lambda console editor to match.
  • Click Deploy to save and deploy your function.
  • Verify your fix by clicking Test.

You should see a 200 status code and a valid JSON response with fact, category, and timestamp fields in the response body.

Still seeing a failed test?

  • Make sure you clicked Deploy before clicking Test. The Lambda console does not auto-save your code changes.
  • Check that body uses json.dumps(body) and not just body by itself. Returning a raw dictionary instead of a string is the most common mistake.
  • Verify that statusCode is 200 (an integer), not "200" (a string in quotes).
  • help me debug my Lambda function's response format
Deploy your API to a production stage

Your Lambda function is returning the correct format. Now you need to deploy your API Gateway so the outside world can reach it.

  • Switch back to the API Gateway console tab in your browser.
  • Make sure your FunFactsAPI is selected in the APIs list.
  • In the Resources pane, click Deploy API.
  • For Stage, select New stage.
  • For Stage name, enter prod.
  • Click Deploy.

What is a stage?

A stage is a named snapshot of your API that makes it callable at a URL. Think of it like publishing a draft. Your API existed in the console, but nobody outside AWS could reach it until you deployed it to a stage.

Teams often create separate stages like dev, staging, and prod to test changes before pushing them to real users.

After deployment, you land on the Stages page. Your Invoke URL is displayed under Stage details.

  • Under Stage details, click the copy icon next to your Invoke URL to copy it.
  • Record your Invoke URL here: your Invoke URL

What does the Invoke URL look like?

Your Invoke URL follows this format: https://abc123.execute-api.us-east-1.amazonaws.com/prod. The first part is a unique ID for your API. The region matches where you created it. The /prod at the end is the stage name you just set.

Test your live API from a browser and terminal

Your API is now live on the internet. Anyone with the URL can hit it. Before you run your first real test, take a moment to think about what you expect to see.

Do you think the response will look the same as it did in the Lambda console test, or will it be different? Why?

  • Paste your Invoke URL into a new browser tab and press Enter.

Your browser sends a GET request to API Gateway, which triggers your Lambda function. You should see raw JSON displayed directly in the browser with a random fun fact, a category, and a timestamp.

  • Refresh the page a few times.

Each refresh triggers a new invocation of your Lambda function. You should see a different fun fact each time.

  • Now test with curl by opening PowerShell. Press the Windows key, type PowerShell, and press Enter.
  • Test your API from the terminal by running this command:
curl.exe [[INVOKE_URL="your Invoke URL"]]

Why curl.exe and not curl?

PowerShell has a built-in alias called curl that actually runs Invoke-WebRequest. Using curl.exe explicitly calls the real curl program, which gives you clean JSON output instead of a PowerShell object.

You should see the same JSON response in your terminal that you saw in your browser. A random fun fact, a category, and a UTC timestamp.

Not seeing JSON in your browser?

  • Make sure you copied the full Invoke URL, including the /prod at the end.
  • If you see a Missing Authentication Token message, double-check that you have a GET method configured on the root resource (/) in your API Gateway. This error often means API Gateway received a request for a path or method that does not exist.
  • If the browser shows an error page, go back to the API Gateway console and confirm that the prod stage exists under Stages in the left navigation pane.
  • help me troubleshoot my API Gateway Invoke URL

Your serverless API is live. You just deployed a Python function to the cloud, connected it to a public URL, and proved it works from both a browser and a terminal. Next up, you will push your API further with an optional Secret Mission.

Secret mission

Add Category Filtering with Query Parameters

Your API returns a random fact from every category each time. Real-world APIs let users request specific data. In this mission, you will add query parameter support so users can append ?category=programming to your URL and get only programming-related facts.

Clean Up Your Resources

Clean Up Your Resources

All resources in this project are covered by the AWS Free Tier, so there are no ongoing costs while your usage stays within the free limits. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • AWS Lambda function (funFactsAPI).
  • API Gateway REST API (FunFactsAPI) with a prod stage.
  • IAM execution role (auto-created by Lambda).

Keep everything running

No action needed. Choose this if you want to keep your API live and accessible.

  • Your Fun Facts API stays live at its Invoke URL at no cost within the free tier limits.
  • Lambda is always free for up to 1,000,000 requests per month.
  • API Gateway is free for 1,000,000 REST API calls per month for the first 12 months after creating your AWS account.
  • You can share your Invoke URL in a portfolio or resume to demonstrate your cloud skills.

Pause - I'll come back to this later

There is nothing to pause for this project. Lambda and API Gateway are event-driven services that only incur charges when they receive requests.

  • When nobody sends requests to your API, these services sit idle at no cost.
  • Your resources will be exactly as you left them when you come back.
  • If you want to prevent any accidental usage, delete the API Gateway API from the Delete tab and keep the Lambda function for later.

Delete - I don't want to use this again

Remove all project resources from your AWS account to start fresh.

  • Return to the Lambda console at https://console.aws.amazon.com/lambda.
  • Select the funFactsAPI function from the list.
  • Delete the function and confirm when prompted.
  • Return to the API Gateway console at https://console.aws.amazon.com/apigateway.
  • Select the FunFactsAPI API from the list.
  • Delete the API and confirm when prompted.
  • (Optional) Search for IAM in the AWS console search bar and open the IAM console.
  • Select Roles in the left sidebar and find the execution role that was auto-created for your funFactsAPI function.
  • Delete the role and confirm when prompted.

Nice Work!

Nice Work!

You just built and deployed a serverless API from scratch. Your Fun Facts API is live on the internet, backed by Python, and running on AWS without a single server for you to manage.

You've learned how to:

  • Build a Python Lambda function that processes requests in the cloud with zero server management.
  • Connect API Gateway to your function to create a live REST API endpoint anyone on the internet can reach.
  • Debug a 502 Bad Gateway error by learning the exact response format Lambda and API Gateway use to communicate.
  • Secret Mission: Added query parameter filtering so users can request facts by category, a real-world API pattern you will use in every API you build.

Ready to quiz yourself?