Build a REST API with FastAPI and Lambda

Build and deploy a serverless CRUD API to AWS Lambda using FastAPI.

Introduction

30 Second Summary

Think about how apps like Goodreads or your local library's catalog let you search, add, and manage books from anywhere. Behind every app like that is an API handling those requests.

In this project, you will build a REST API that manages a book collection, then deploy it to the cloud so anyone on the internet can access it.

What You'll Build

A Book Collection REST API with full create, read, update, and delete functionality, built with FastAPI and deployed to AWS Lambda.

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

  • A FastAPI application with 5 endpoints for creating, reading, updating, and deleting books.
  • Pydantic data models that automatically validate every request to your API.
  • A live AWS Lambda deployment with a public HTTPS endpoint accessible from anywhere on the internet.
  • Secret Mission: A search endpoint that filters books by author or title.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, dictionaries). No prior AWS or API experience is needed.

Before We Start

Every web application needs a backend to handle data. In this project, you will build a REST API that manages a book collection and deploy it to the cloud so anyone can use it.

To get there, you need three things on your machine: Python (the language), a code editor, and a few Python packages. This step gets all of that ready.

In this step, get ready to:
  • Install Python 3.12+ and verify it works.
  • Install Cursor and create your project folder.
  • Install FastAPI and Mangum inside a virtual environment.
Install Python 3.12+

Python is the programming language you will use to write your API. This project requires version 3.12 or higher because the code uses modern syntax like str | None and dict[int, dict].

  • Open a terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or Command Prompt (Windows), and pressing Enter.
  • Check your Python version by running this command:

macOS

python3 --version

On macOS, the Python command is python3.

Windows

python --version

On Windows, the Python command is python.

✔️ I see version 3.12 or higher

You are all set. Move on to the next section below.

ⓧ I see an older version

Your machine has Python installed, but at a version below 3.12. You need to install a newer version.

  • Go to python.org/downloads and download the latest Python 3 installer for your operating system.
  • Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install.
  • Close your terminal and open a new one, then re-run the version check command above.

Why a new terminal?

PATH changes from an installer only take effect in new terminal sessions. The old terminal still sees the previous version.

On macOS, the system python3 may still point to the old version even after installing a new one. If so, use the full path to the newly installed Python (for example, /Library/Frameworks/Python.framework/Versions/3.12/bin/python3) or install a version manager like pyenv.

ⓧ Command not found

Python is not installed on your machine yet. Follow these steps to install it.

  • Go to python.org/downloads and download the latest Python 3 installer for your operating system.
  • Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install.
  • On macOS, double-click the downloaded .pkg file and follow the prompts.
  • Close your terminal and open a new one, then re-run the version check command above.

You should now see a version number of 3.12 or higher.

Still not seeing the right version?

Make sure you opened a brand new terminal window after installing. On Windows, also check that the App Execution Aliases for Python are disabled in Settings > Apps > App execution aliases if you see a Microsoft Store prompt instead of a version number.

help me troubleshoot my Python installation

Install Cursor and create your project folder

Cursor is a code editor built for working with AI. You will use it to write and edit your API code throughout this project.

  • Go to cursor.com/download in your browser.
  • Click the download button for your operating system.
  • Open the downloaded file and follow the install steps for your OS. On macOS, drag Cursor into your Applications folder. On Windows, run the installer and follow the prompts.
  • Open Cursor by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Cursor, and pressing Enter.
  • Sign in with your Cursor account when prompted. If you do not have one, sign up for free at cursor.com.

Now create your project folder and open it in Cursor.

  • Open the built-in terminal inside Cursor by pressing Ctrl+` (both macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir book-collection-api
cd book-collection-api
  • Open this folder in Cursor by clicking File in the top menu bar.
  • Click Open Folder.
  • Select the book-collection-api folder on your Desktop, then click Open.

Why a dedicated project folder?

Keeping all project files in one folder makes it easy to package everything for deployment later. Your API code, dependencies, and configuration will all live inside book-collection-api.

  • Confirm the folder exists by running:
ls

You should see an empty listing since the folder is new. That is expected.

Create a virtual environment and install dependencies

A virtual environment is an isolated space where Python packages are installed without affecting the rest of your system. This keeps your project's dependencies separate and makes packaging for deployment straightforward.

  • Make sure your terminal is inside the book-collection-api folder.
  • Create a virtual environment by running this command:

macOS

python3 -m venv venv

Windows

python -m venv venv

What does this command do?

The -m venv flag tells Python to run its built-in virtual environment module. The second venv is the name of the folder that will hold the isolated environment.

  • Activate the virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Not seeing (venv) in your prompt?

On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try the activate command again.

help me activate my Python virtual environment

  • Install FastAPI and Mangum by running this command:
pip install "fastapi[standard]==0.136.1" mangum==0.21.0

What are these packages?

FastAPI is a modern Python framework for building APIs. The [standard] extra includes Uvicorn (the development server) and other useful tools.

Mangum is a lightweight adapter that lets your FastAPI app run inside AWS Lambda. You will use it in the deployment step.

After the install finishes, you should see output ending with a line like Successfully installed followed by a list of package names.

Seeing a permission error or install failure?

Make sure the (venv) prefix is visible in your terminal prompt. If it is not, re-run the activate command from the previous section. Installing without an active virtual environment can cause permission errors.

help me fix my pip install error

✔️ Awesome, I've got everything!

Great. Make sure you have saved nothing yet (there are no files to save), but confirm your terminal shows (venv) and the install completed successfully.

ⓧ I'd like to double check my setup

Run the following command to confirm both packages are installed:

pip show fastapi mangum

You should see two blocks of output. Check that the Version line shows 0.136.1 for fastapi and 0.21.0 for mangum.

Your development environment is ready. Next up, you will write your FastAPI application with full CRUD endpoints for managing books.

Build Your FastAPI CRUD Endpoints

Your environment is set up and your dependencies are installed. Now it's time to write the actual API.

You'll build a RESTful API that manages a book collection. Each endpoint will handle one of the CRUD operations: Create, Read, Update, and Delete.

In this step, get ready to:
  • Create main.py with a FastAPI app and Pydantic data models.
  • Implement 5 CRUD endpoints for managing books.
  • Use an in-memory dictionary as the data store.
Set up the FastAPI app and data models

Every API needs a foundation: an app instance, a data store, and models that define the shape of your data.

  • In Cursor, create a new file called main.py by hovering over your project folder in the Explorer sidebar and clicking the new file icon.
  • Copy the following code into main.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Book Collection API")

# In-memory data store
books_db: dict[int, dict] = {}
next_id: int = 1


class BookCreate(BaseModel):
    title: str
    author: str
    year: int


class BookUpdate(BaseModel):
    title: str | None = None
    author: str | None = None
    year: int | None = None


class BookResponse(BaseModel):
    id: int
    title: str
    author: str
    year: int

What does this code do?

This sets up the foundation for your API:

  • app = FastAPI(title="Book Collection API") creates your FastAPI application instance. The title appears in the auto-generated docs.
  • books_db is a Python dictionary that stores your books in memory. This keeps the project focused on API design without needing a database.
  • next_id tracks the next available ID for new books, incrementing with each creation.
  • The three Pydantic classes define data shapes: BookCreate for incoming new books, BookUpdate for partial edits (all fields optional), and BookResponse for what the API sends back.

Why use an in-memory dictionary?

Lambda functions are stateless. Data resets between cold starts. Using a dictionary keeps the project focused on API design and deployment patterns.

In production, you would swap this for a real database like DynamoDB or RDS. For learning REST and serverless deployment, a dictionary is perfect.

  • Save main.py.
  • You won't see any output yet. The next chunk adds the endpoints that bring this foundation to life.
Add the read endpoints

Read endpoints let clients retrieve data from your API. You'll create two: one that lists all books and another that fetches a single book by its ID.

  • In main.py, add the following code below the BookResponse class:
@app.get("/books", response_model=list[BookResponse])
def list_books():
    return [{"id": book_id, **data} for book_id, data in books_db.items()]


@app.get("/books/{book_id}", response_model=BookResponse)
def get_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    return {"id": book_id, **books_db[book_id]}

What does this code do?

  • GET /books returns every book in the store. The response_model tells FastAPI to validate and format the output as a list of BookResponse objects.
  • GET /books/{book_id} fetches one book. The {book_id} in the path is a path parameter that FastAPI automatically converts to an integer.
  • If the requested ID doesn't exist, the endpoint raises a 404 error with a clear message. This is standard REST behavior for missing resources.
  • Save main.py.
  • You won't see output yet. The next chunk adds the write endpoints that create, update, and delete books.
Add the write endpoints

Write endpoints let clients modify data. You'll add three: one to create a book, one to update it, and one to delete it.

  • In main.py, add the following code below the get_book function:
@app.post("/books", response_model=BookResponse, status_code=201)
def create_book(book: BookCreate):
    global next_id
    books_db[next_id] = book.model_dump()
    response = {"id": next_id, **books_db[next_id]}
    next_id += 1
    return response


@app.put("/books/{book_id}", response_model=BookResponse)
def update_book(book_id: int, book: BookUpdate):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    update_data = book.model_dump(exclude_unset=True)
    books_db[book_id].update(update_data)
    return {"id": book_id, **books_db[book_id]}


@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    del books_db[book_id]

What does this code do?

  • POST /books creates a new book. FastAPI validates the request body against BookCreate automatically. The status_code=201 signals "resource created" per REST conventions.
  • book.model_dump() converts the Pydantic model into a plain dictionary for storage.
  • PUT /books/{book_id} updates an existing book. The exclude_unset=True parameter means only the fields the client actually sends get updated. Missing fields stay unchanged.
  • DELETE /books/{book_id} removes a book. The 204 status code means "success, no content to return."
  • Save main.py.
  • Confirm there are no red underlines (syntax errors) in Cursor. Your file should have all imports at the top, three model classes, and five endpoint functions.

Seeing red underlines in your code?

  • Make sure your virtual environment is active. Cursor needs the environment with FastAPI installed to resolve imports.
  • Check that from fastapi import FastAPI, HTTPException is on the very first line. A typo in the import will cascade errors.
  • Confirm you installed FastAPI with the standard extras: pip install "fastapi[standard]==0.136.1".

Help me fix import errors in my FastAPI project

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and move on to testing.

ⓧ I'd like to double check the full code

Here is the complete main.py file at this point:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Book Collection API")

# In-memory data store
books_db: dict[int, dict] = {}
next_id: int = 1


class BookCreate(BaseModel):
    title: str
    author: str
    year: int


class BookUpdate(BaseModel):
    title: str | None = None
    author: str | None = None
    year: int | None = None


class BookResponse(BaseModel):
    id: int
    title: str
    author: str
    year: int


@app.get("/books", response_model=list[BookResponse])
def list_books():
    return [{"id": book_id, **data} for book_id, data in books_db.items()]


@app.get("/books/{book_id}", response_model=BookResponse)
def get_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    return {"id": book_id, **books_db[book_id]}


@app.post("/books", response_model=BookResponse, status_code=201)
def create_book(book: BookCreate):
    global next_id
    books_db[next_id] = book.model_dump()
    response = {"id": next_id, **books_db[next_id]}
    next_id += 1
    return response


@app.put("/books/{book_id}", response_model=BookResponse)
def update_book(book_id: int, book: BookUpdate):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    update_data = book.model_dump(exclude_unset=True)
    books_db[book_id].update(update_data)
    return {"id": book_id, **books_db[book_id]}


@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    del books_db[book_id]

Your API code is complete. Next up, you'll start the development server and test every endpoint using FastAPI's built-in interactive docs.

Test Your API Locally

Your API code is complete and ready to run. Before deploying anywhere, you need to verify every endpoint works correctly on your local machine.

FastAPI includes a built-in interactive documentation page called Swagger UI. It lets you send requests to your API directly from the browser without writing any test scripts or using external tools.

In this step, get ready to:
  • Run the FastAPI development server.
  • Test all 5 CRUD endpoints using Swagger UI.
  • Verify create, read, update, and delete operations work correctly.
Start the development server

FastAPI comes with a development server that auto-reloads whenever you change your code. It also generates interactive API documentation automatically from your endpoint definitions.

  • In the terminal in Cursor (with your virtual environment still active), start the server by running this command:
fastapi dev main.py

You should see output including a line that says Uvicorn running on http://127.0.0.1:8000. This means your API is now running locally.

What does this command do?

The fastapi dev command starts a Uvicorn development server with auto-reload enabled. It automatically detects the app object in your main.py file.

The server listens on 127.0.0.1:8000 (localhost only). It also generates interactive documentation at /docs based on your endpoint decorators and Pydantic models.

Keep this terminal running

The server occupies the terminal while it runs. Do not close it or press Ctrl+C until you are done testing. If you need to run other commands, open a second terminal.

Seeing ModuleNotFoundError?

Make sure your virtual environment is still active. You should see the environment name in parentheses at the start of your terminal prompt. If it is missing, reactivate it before running the command again.

help me fix a ModuleNotFoundError when running fastapi dev main.py

Open the Swagger UI documentation

FastAPI automatically generates an interactive testing page from your code. Every endpoint, request body, and response schema you defined shows up here.

  • Open your browser.
  • Navigate to http://127.0.0.1:8000/docs.

You should see a page titled "Book Collection API" with all 5 of your endpoints listed: GET /books, GET /books/{book_id}, POST /books, PUT /books/{book_id}, and DELETE /books/{book_id}.

What is Swagger UI?

Swagger UI is an interactive documentation tool that reads the OpenAPI schema FastAPI generates from your code. It shows every endpoint, what parameters it expects, and what it returns.

You can send real requests directly from this page by clicking on an endpoint, filling in the parameters, and pressing Execute. The response appears right below.

Test all CRUD operations

Now walk through the full lifecycle: create a book, retrieve it, update it, and delete it. This confirms every endpoint handles requests correctly.

  • Click on the POST /books endpoint to expand it.
  • Click Try it out.
  • Replace the example JSON in the request body with this:
{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "year": 1925
}
  • Click Execute.

You should see a response with status code 201 and a JSON body that includes an id field (the value should be 1).

  • Click on the GET /books endpoint to expand it.
  • Click Try it out, then Execute.

You should see a JSON array containing the book you just created.

  • Click on the PUT /books/{book_id} endpoint.
  • Click Try it out.
  • Enter 1 in the book_id field.
  • Replace the request body with this:
{
  "year": 2025
}
  • Click Execute.

You should see a 200 response with the book's year updated to 2025, while title and author remain unchanged.

  • Click on the DELETE /books/{book_id} endpoint.
  • Click Try it out.
  • Enter 1 in the book_id field.
  • Click Execute.

You should see a 204 response with no body. This means the book was deleted successfully.

  • Confirm deletion by expanding GET /books again and clicking Execute.

You should see an empty array [], confirming the book no longer exists.

Why does this prove the API works?

You just completed the full CRUD lifecycle: Create (POST), Read (GET), Update (PUT), and Delete (DELETE). Each operation returned the correct HTTP status code and response body.

If the GET after delete returns an empty list, that confirms all four operations modified the data store correctly.

Getting a 422 Validation Error?

A 422 response means your request body does not match the Pydantic model. Check that your JSON uses the exact field names title, author, and year with the correct types (strings for the first two, an integer for year).

help me fix a 422 validation error when testing my FastAPI CRUD endpoints in Swagger UI

  • Return to your terminal and press Ctrl+C to stop the development server.

Your API is verified and working locally. Next up, you will add the Mangum adapter so this same API can run inside AWS Lambda.

Add the Mangum Lambda Handler

Your API runs perfectly on your local machine, but AWS Lambda does not natively understand ASGI frameworks like FastAPI. Lambda expects a specific handler function that receives an event and returns a response.

Mangum is a lightweight adapter that translates Lambda events into ASGI requests that FastAPI understands. In this step, you will add the Mangum handler, pin your dependencies, and package everything into a ZIP file ready for deployment.

In this step, get ready to:
  • Add the Mangum adapter to wrap your FastAPI app for Lambda.
  • Create a requirements file with pinned dependencies.
  • Package the application into a ZIP for deployment.
Add the Mangum adapter to main.py

You need two small additions to main.py to make it Lambda-compatible.

  • In main.py, find the import lines at the top of your file:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
  • Add this new import line directly below from pydantic import BaseModel:
from mangum import Mangum
  • Now scroll to the very bottom of main.py, after the delete_book function.
  • Add an empty line after the last line of the function, then add this handler:
handler = Mangum(app, lifespan="off")

What does this code do?

The from mangum import Mangum line imports the adapter that bridges ASGI and Lambda.

The handler = Mangum(app, lifespan="off") line creates the function that Lambda will invoke. It wraps your FastAPI app so incoming Lambda events get translated into HTTP requests that your endpoints can process. The lifespan="off" setting disables ASGI startup/shutdown events, which are not meaningful in Lambda's stateless execution model.

  • Save main.py.
  • You will not see a visible change yet. The handler only activates when Lambda invokes your function, which you will set up in the next step.

Seeing an import error?

If your editor underlines from mangum import Mangum with a red squiggle, make sure your virtual environment is active and that you installed Mangum in Step 1. Rerun pip install mangum==0.21.0 if needed.

Still stuck? Help me fix my Mangum import error in main.py

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py before moving on.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from mangum import Mangum

app = FastAPI(title="Book Collection API")

# In-memory data store
books_db: dict[int, dict] = {}
next_id: int = 1


class BookCreate(BaseModel):
    title: str
    author: str
    year: int


class BookUpdate(BaseModel):
    title: str | None = None
    author: str | None = None
    year: int | None = None


class BookResponse(BaseModel):
    id: int
    title: str
    author: str
    year: int


@app.get("/books", response_model=list[BookResponse])
def list_books():
    return [{"id": book_id, **data} for book_id, data in books_db.items()]


@app.get("/books/{book_id}", response_model=BookResponse)
def get_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    return {"id": book_id, **books_db[book_id]}


@app.post("/books", response_model=BookResponse, status_code=201)
def create_book(book: BookCreate):
    global next_id
    books_db[next_id] = book.model_dump()
    response = {"id": next_id, **books_db[next_id]}
    next_id += 1
    return response


@app.put("/books/{book_id}", response_model=BookResponse)
def update_book(book_id: int, book: BookUpdate):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    update_data = book.model_dump(exclude_unset=True)
    books_db[book_id].update(update_data)
    return {"id": book_id, **books_db[book_id]}


@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    del books_db[book_id]


handler = Mangum(app, lifespan="off")
Create requirements.txt with pinned dependencies

Lambda needs to know exactly which packages your app depends on. A requirements.txt file locks your dependencies to specific versions so deployments are reproducible.

  • Create a new file called requirements.txt in your project folder (the same folder as main.py).
  • Paste the following into requirements.txt:
fastapi==0.136.1
mangum==0.21.0

Why pin exact versions?

Pinning with == ensures that every deployment installs the exact same packages. Without pins, a future pip install could pull a newer version that introduces breaking changes.

  • Save requirements.txt.
  • Confirm the file exists by running:
ls

You should see both main.py and requirements.txt in the output.

Don't see requirements.txt?

Make sure you created the file in the same folder as main.py. Check your terminal's current directory matches your project folder.

Need help? Help me find where my requirements.txt ended up

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before packaging.

ⓧ I'd like to double check the full code

fastapi==0.136.1
mangum==0.21.0
Package the application into a ZIP file

Lambda expects a single ZIP file containing your code and all its dependencies at the root level. You will install the dependencies into a local folder, add your main.py, and zip everything together.

  • Install your dependencies into a package folder by running:
pip install -r requirements.txt -t package/

What does -t do?

The -t package/ flag tells pip to install packages into the package/ directory instead of your virtual environment. This isolates the Lambda dependencies so you can bundle them into a ZIP without including unrelated files from your virtual environment.

  • Copy your application code into the package folder and create the deployment ZIP by running the commands for your operating system:

macOS

cp main.py package/
cd package
zip -r ../deployment.zip .
cd ..

Windows

Copy-Item main.py package/
cd package
Compress-Archive -Path * -DestinationPath ..\deployment.zip
cd ..

Why zip from inside the package folder?

Lambda expects all files to be at the root level of the ZIP archive. By changing into the package/ directory before zipping, the files end up at the root of the ZIP rather than nested inside a package/ subfolder.

  • Confirm the ZIP file was created by running:
ls

You should see deployment.zip listed in the output alongside main.py, requirements.txt, and the package/ folder.

Don't see deployment.zip?

Make sure you ran the cd .. command at the end to return to your project folder before running ls. If you are still inside the package/ directory, the ZIP will not be visible.

On Windows, if Compress-Archive reported an error, make sure you have files inside the package/ folder by running ls package/ first.

Still stuck? Help me create my Lambda deployment ZIP

Your deployment package is ready to go. Next up, you will upload this ZIP to AWS Lambda and get a live public URL for your API.

Deploy to AWS Lambda

Your API works locally. Now it is time to put it on the internet where anyone can access it.

In this step, you will deploy your application to AWS Lambda, a serverless compute service. Lambda runs your code only when someone sends a request, so you pay nothing when the API is idle.

In this step, get ready to:
  • Create a Lambda function and upload your deployment ZIP.
  • Configure a public Function URL so anyone can reach your API.
  • Test the live endpoint to confirm all CRUD operations work.
Create a Lambda function and upload your ZIP

AWS Lambda needs a function resource to run your code. You will create one, upload the deployment package you built in the previous step, and point Lambda at the correct handler.

  • Open your browser and navigate to the AWS Management Console at https://console.aws.amazon.com.
  • Sign in to your AWS account (or create a free account if you do not have one).
  • In the search bar at the top of the console, type Lambda and select Lambda from the results.
  • On the Functions page, click Create function.
  • Select Author from scratch.
  • For Function name, enter book-collection-api.
  • For Runtime, select Python 3.12.
  • Leave all other settings at their defaults.
  • Click Create function.

Lambda creates a placeholder function. Next you will replace its code with your deployment ZIP.

  • On your function's page, select the Code tab.
  • In the Code source pane, click Upload from.
  • Select .zip file.
  • Click Upload and select the deployment.zip file from your project folder.
  • Click Open.
  • Click Save.

Lambda now has your code, but it still points at the default handler. You need to update it to match your file.

  • Scroll down to the Runtime settings pane and click Edit.
  • In the Handler field, replace the existing value with main.handler.
  • Click Save.

Why main.handler?

The handler format for Python Lambda functions is filename.variable_name. Your file is main.py and the Mangum adapter is stored in a variable called handler.

Lambda uses this path to find the entry point for every incoming request.

Seeing a module import error?

If you see an error about missing modules when testing, your ZIP file may not have dependencies at the root level. Re-create your ZIP making sure main.py and all installed packages sit at the top level of the archive, not inside a subfolder.

Help me fix my deployment package structure

Configure a Function URL for public access

Your function exists but has no public address. A Lambda Function URL gives your function a dedicated HTTPS endpoint that anyone on the internet can call.

  • On your function's page, select the Configuration tab.
  • In the left sidebar, click Function URL.
  • Click Create function URL.
  • For Auth type, select NONE.
  • Click Save.

Your Function URL is now displayed on the page. It looks like https://<url-id>.lambda-url.<region>.on.aws. Copy this URL. You will use it to test your live API.

What does Auth type NONE mean?

Setting Auth type to NONE allows anyone with the URL to invoke your function without AWS credentials. Lambda automatically creates the required resource-based policy that grants public access.

This is perfect for a learning project, but production APIs typically use authentication.

Don't see the Function URL option?

Make sure you are on the Configuration tab (not the Code tab). The Function URL option appears in the left sidebar of the Configuration tab.

Help me find the Function URL option in my Lambda console

Test the live API

Time to confirm everything works end to end. You will run the same CRUD operations against your live Function URL.

  • Return to your terminal in Cursor.
  • Create a book by running this command (replace the URL with your Function URL):
curl -X POST [[FUNCTION_URL="your Function URL"]]/books \
  -H "Content-Type: application/json" \
  -d '{"title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925}'

You should see a JSON response with an id field, confirming the book was created.

  • List all books by running:
curl [[FUNCTION_URL="your Function URL"]]/books

You should see a JSON array containing the book you just created.

  • Test the update endpoint by running:
curl -X PUT [[FUNCTION_URL="your Function URL"]]/books/1 \
  -H "Content-Type: application/json" \
  -d '{"year": 2025}'

You should see the updated book with year set to 2025.

  • Delete the book by running:
curl -X DELETE [[FUNCTION_URL="your Function URL"]]/books/1

A successful delete returns no body with a 204 status code.

  • Verify the delete worked by listing books again:
curl [[FUNCTION_URL="your Function URL"]]/books

You should see an empty list [] confirming the book was removed.

Why does the data reset between deploys?

Your API uses an in-memory dictionary as its data store. Lambda functions are stateless, so the data resets whenever the function's execution environment is recycled (a "cold start").

In a production application, you would connect to an external database like DynamoDB or RDS.

Test in your browser too

You can paste your Function URL followed by /books directly into your browser's address bar to see the GET response. Browsers send GET requests by default.

Getting an Internal Server Error?

Check that your handler is set to main.handler in the Runtime settings. Also verify that your ZIP file has main.py and all dependencies at the root of the archive, not nested inside a folder.

Help me debug my Lambda Internal Server Error

Your API is live on the internet. Anyone with your Function URL can create, read, update, and delete books. Next up, you will tackle a secret mission to add search functionality.

Secret mission

Add a Search Endpoint

Your API can create, read, update, and delete books. But what if someone wants to find all books by a specific author? Build a search endpoint that filters the collection by author or title with case-insensitive partial matching.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Your Lambda function sits within the AWS Always Free tier (1 million requests per month and 400,000 GB-seconds of compute). It costs nothing when idle and only consumes free tier allocation when invoked.

Resources you used:

  • AWS Lambda function book-collection-api (includes Function URL).
  • Lambda execution role (auto-created when you built the function).
  • Local project folder on your Desktop (contains main.py, requirements.txt, virtual environment, and deployment ZIP).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your API or show it to others.

  • Your Lambda function costs nothing when idle. You only use free tier allocation when someone sends a request to your Function URL.
  • Your local project files stay on your Desktop for future reference or iteration.

Pause - I'll come back to this later

Lambda functions are serverless and only run when invoked. There is nothing to pause.

  • Your function already sits idle between requests. No ongoing processes consume resources.
  • If you want to free up local disk space, you can deactivate your virtual environment by running:
deactivate

Your project files remain intact for when you come back.

Delete - I don't want to use this again

Remove all project resources and start fresh. This cannot be undone.

  • Delete the Lambda function:
  • Open the Functions page of the Lambda console.
  • Select book-collection-api.
  • Choose Actions, then Delete.
  • Type confirm in the text input field and choose Delete.

What about the Function URL?

When you delete a Lambda function, AWS asynchronously deletes its Function URL. You do not need to remove the Function URL separately.

  • Delete the execution role that Lambda auto-created:
  • Open the Roles page of the IAM console.
  • Select the execution role for your function (it has the format book-collection-api-role- followed by random characters).
  • Choose Delete.
  • Enter the role name in the text input field and choose Delete.
  • Delete your local project folder by running these commands in your terminal:

macOS

deactivate
cd ~/Desktop
rm -rf book-collection-api

Windows

deactivate
cd ~\Desktop
rmdir /s /q book-collection-api

Folder name different?

Replace the folder name in the command above with whatever you named your project folder on the Desktop.

That's a Wrap!

That's a Wrap!

Nice work! You just built a serverless REST API from scratch and deployed it to a live public endpoint on AWS.

You've learned how to:

  • Built a FastAPI application with full CRUD operations and Pydantic data validation.
  • Used Mangum as an ASGI adapter to bridge FastAPI to AWS Lambda.
  • Deployed your API to a live public HTTPS endpoint using a Lambda Function URL accessible from anywhere on the internet.
  • Added a search endpoint with query parameter filtering and case-insensitive partial matching.

Ready to quiz yourself?