Build an Event Booking API with FastAPI

Build and test a FastAPI event booking API with validation and error handling.

Introduction

30 Second Summary

An event can look available right until two people claim the final seat. Everyone still needs a clear answer about whether their booking succeeded.

In this project, you will build a local event booking API with FastAPI. You will turn booking rules into clear responses that automated tests can protect.

What You'll Build

Your browser becomes a control panel where a successful booking instantly reduces the chosen event's remaining capacity in JSON.

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

  • A live event catalogue that shows every seeded event with its current capacity as JSON.
  • A booking flow that returns a generated booking ID while reducing the selected event's available seats.
  • A test suite you can run to prove successful bookings, malformed requests, missing events, and sold-out events follow the API contract.
  • Secret Mission: Add an availability filter that hides sold-out events while preserving the full event list by default.

Are there any prerequisites?

You should be comfortable with Python functions, classes, type hints, and dictionaries. The project assumes Python, Visual Studio Code, and uv are already installed on Windows.

Before We Start

Before the hands-on work begins, take a moment to commit to the event booking API you are building. Your goal gives every route and validation rule a clear purpose.

Set Up a Pinned FastAPI Project

Your event booking API depends on Python packages that can change over time. A reproducible environment keeps package drift from hiding the backend concepts you want to learn.

Every event route you build later relies on the same dependency contract. Visual Studio Code keeps the project workspace together while uv creates the pinned environment.

In this step, get ready to:
  • Create a Visual Studio Code workspace for the event booking API.
  • Define the project's exact dependency contract.
  • Produce a synced environment whose dependency tree matches the contract.
Define the dependency contract

A project contract records the Python requirement plus every direct package version. The pyproject.toml file gives uv one source of truth for the environment.

  • Press the Windows key to open the search bar.
  • Type Visual Studio Code into the search bar.
  • Press Enter to open Visual Studio Code.

You'll see the Visual Studio Code window open.

  • Select File from the top menu.
  • Select Open Folder....
  • Select your Desktop in the folder dialog.
  • Select New Folder.
  • Enter event-booking-api as the folder name.
  • Press Enter to create the folder.

Trust Your Own Workspace

Visual Studio Code may show a Workspace Trust dialog after you open the folder. You created this empty folder yourself, so enabling workspace features is appropriate.

  • Select Select Folder.
  • Select Yes, I trust the authors if the Workspace Trust dialog appears.

You'll see event-booking-api at the top of the Explorer sidebar. This confirms Visual Studio Code has opened the correct workspace.

  • Select New File... in the Explorer sidebar.
  • Enter pyproject.toml as the file name.
  • Press Enter to create the file inside event-booking-api.
  • Define the project's dependency contract by pasting this configuration into pyproject.toml:
[project]
name = "event-booking-api"
version = "0.1.0"
description = "A focused FastAPI event booking project"
requires-python = ">=3.10"
dependencies = [
    "fastapi[standard-no-fastapi-cloud-cli]==0.162.0",
    "httpx==0.28.1",
    "pydantic==2.14.1",
]

[dependency-groups]
dev = [
    "pytest==9.2.0",
]

What Does This Configuration Lock Down?

  • The project requires Python >=3.10.
  • FastAPI 0.162.0 provides the API framework plus its local development command.
  • Pydantic 2.14.1 defines typed data contracts.
  • HTTPX 0.28.1 supports the test requests you use later.
  • The pytest package at 9.2.0 provides the automated test runner.
  • Exact pins make repeated environment syncs resolve the same direct versions.
  • Save pyproject.toml by pressing Ctrl+S.
  • Confirm the Explorer sidebar lists pyproject.toml inside event-booking-api.

Can't See the Project File?

Confirm the Explorer heading shows event-booking-api. Reopen that folder if Visual Studio Code shows a different workspace.

Check that the file name is exactly pyproject.toml.

Help me find why my project folder or pyproject.toml file is missing.

✔️ Awesome, I've got everything!

Great. Your exact dependency contract is saved inside the correct project folder.

ⓧ I'd like to double check the full code

Compare your complete pyproject.toml file with this reference.

[project]
name = "event-booking-api"
version = "0.1.0"
description = "A focused FastAPI event booking project"
requires-python = ">=3.10"
dependencies = [
    "fastapi[standard-no-fastapi-cloud-cli]==0.162.0",
    "httpx==0.28.1",
    "pydantic==2.14.1",
]

[dependency-groups]
dev = [
    "pytest==9.2.0",
]

What Should Match?

Every package uses an exact version pin. The development dependency stays inside the dev dependency group.

Create the local environment

A virtual environment isolates this project's packages from the rest of your computer. The lockfile records the resolved environment so future syncs can reproduce it.

  • Press Ctrl+` to open the integrated terminal.
  • Confirm the terminal path ends with event-booking-api.
  • Sync the pinned project environment by running this command:
uv sync

What Does the Sync Create?

  • The command reads the dependency contract from pyproject.toml.
  • It creates .venv for the isolated project environment.
  • It generates uv.lock with the resolved package graph.
  • The default sync includes the dev dependency group.
  • Confirm the Explorer sidebar lists .venv.
  • Confirm the Explorer sidebar lists uv.lock.

That's the environment created. Your application packages plus testing tools now live inside the project workspace.

Sync Didn't Complete?

Confirm the terminal path ends with event-booking-api. The sync needs to run beside pyproject.toml.

Save pyproject.toml before trying the sync again.

Help me troubleshoot my failed uv sync.

Verify the resolved versions

A dependency tree separates the packages you pinned from the supporting packages they install. Reading it confirms that the environment matches your contract.

Before you run the check, do you expect the dependency tree to show all four direct packages at their pinned versions?

  • Display the resolved dependency tree by running this command:
uv tree

How to Read the Tree

  • FastAPI appears at version 0.162.0.
  • Pydantic appears at version 2.14.1.
  • HTTPX appears at version 0.28.1.
  • pytest appears at version 9.2.0.
  • Indented entries show supporting packages installed for those direct dependencies.

Everything lines up. Your local environment now matches the project's exact dependency contract.

Missing a Pinned Dependency?

Confirm the earlier sync completed before inspecting the tree.

Compare the direct version pins in pyproject.toml with the full-code reference above.

Help me diagnose a missing or incorrect package in my uv dependency tree.

Your pinned FastAPI environment is ready. Next, you'll turn it into a running API that returns its first event list.

Serve Your First Event List

Your pinned project environment is ready. The project now has a stable base for its first response.

A visible JSON response gives you the smallest useful version of the finished backend. In this step, you will use FastAPI to serve a seeded event collection from memory.

In this step, get ready to:
  • Create the application with in-memory event state.
  • Add a root route that identifies the API.
  • Serve the event collection through the interactive documentation.
Create the FastAPI application state

This first slice stores its data in two Python dictionaries. The events dictionary contains the event records.

The bookings dictionary starts empty. Later requests will add booking records to it.

  • Create a new file named main.py directly inside event-booking-api by using the new-file control in the VS Code file sidebar.

You will see an empty main.py editor tab. The file also appears in the sidebar.

  • Add the application plus its initial state by copying this code into main.py:
from fastapi import FastAPI

app = FastAPI()

events: dict[int, dict] = {
    1: {
        "id": 1,
        "name": "Python Backend Meetup",
        "capacity": 2,
        "booked": 0,
    },
    2: {
        "id": 2,
        "name": "API Design Workshop",
        "capacity": 1,
        "booked": 0,
    },
}

bookings: dict[int, dict] = {}

What Does This Code Do?

  • The FastAPI() call creates the application that receives requests.
  • The events dictionary stores two records under numeric IDs.
  • The bookings dictionary provides a place for booking data later in the project.
  • Save main.py.

You will see main.py remain in the file sidebar without an unsaved-change indicator.

File Missing From the Sidebar?

  • Check that main.py sits directly inside the event-booking-api folder.
  • Check that the filename ends with .py.

Ask for help if the file is still missing: help me place main.py inside my event-booking-api folder in VS Code

Add the API routes

A route connects an HTTP method to a URL path. The root route gives clients a quick way to identify this service.

  • Add the root route below the bookings dictionary by copying this code:
@app.get("/")
def read_root():
    return {"message": "Event Booking API"}

How Does the Root Route Work?

  • The @app.get("/") decorator connects a GET request for the root path to the function below it.
  • The read_root() function returns a message that identifies the API.
  • Save main.py.

The development server keeps the current terminal busy while it runs. Your editor remains available for the next route.

Before you start the server, which route do you expect the interactive documentation to contain?

  • Start the development server from the Visual Studio Code terminal by running this command:
uv run fastapi dev

The terminal reports that the local server is ready. It also provides the address for the interactive documentation.

You will see the GET / route listed. This proves FastAPI discovered the route in main.py.

Server Does Not Start?

  • Confirm that the terminal is still inside the event-booking-api folder.
  • Compare the import plus the app = FastAPI() line with the code above.

Ask for help with the terminal output: help me troubleshoot my FastAPI development server

The event-list route turns the values in events into a list. Each request reads the dictionary's current state.

  • Add the event-list route below read_root() by copying this code:
@app.get("/events")
def list_events():
    return list(events.values())

How Does Event Listing Work?

  • The @app.get("/events") decorator registers the collection route.
  • The list_events() function converts the dictionary values into a JSON-compatible list.
  • Save main.py.
  • Refresh the interactive documentation page from earlier.

You will now see a GET /events route alongside the root route. The development server reloaded your saved code.

Event Route Missing?

  • Confirm that list_events() appears below read_root() in main.py.
  • Confirm that the terminal still shows the development server running.
  • Refresh the documentation page after saving the file.

Ask for help if the route remains missing: help me debug my missing event-list route

Execute the event-list request

The interactive documentation sends a real request to your running application. Its response shows exactly what a client receives from the route.

  • Expand the GET /events route in the interactive documentation.
  • Enable the route's request controls.

Before you send the request, how many event objects do you expect the response to contain?

  • Send the GET /events request.

You will see a JSON array containing two records. One record is for Python Backend Meetup.

The other record is for API Design Workshop. That is your first working API collection route.

✔️ Awesome, I've got everything!

Great. Double check that main.py is saved while the development server continues running.

ⓧ I'd like to double check the full code

Compare your complete main.py file with this version.

from fastapi import FastAPI

app = FastAPI()

events: dict[int, dict] = {
    1: {
        "id": 1,
        "name": "Python Backend Meetup",
        "capacity": 2,
        "booked": 0,
    },
    2: {
        "id": 2,
        "name": "API Design Workshop",
        "capacity": 1,
        "booked": 0,
    },
}

bookings: dict[int, dict] = {}


@app.get("/")
def read_root():
    return {"message": "Event Booking API"}


@app.get("/events")
def list_events():
    return list(events.values())

Your API now serves its first useful collection from in-memory state. Next, you will add event lookup by numeric ID to expose a missing-resource problem.

Expose the Missing-Event Bug

Your event list proves that FastAPI can return the records stored by your application. The next useful capability is retrieving one event by its numeric ID.

Direct dictionary access works when the requested ID exists. Clients can also request IDs that are absent, so this step tests how the same lookup behaves in that case.

In this step, get ready to:
  • Add a numeric path parameter for event lookup.
  • Confirm that direct lookup returns a stored event.
  • Test an unknown event ID and inspect the response.
Add a direct event lookup route

A path parameter captures part of a request URL as a function argument. The type hint on that argument tells FastAPI to interpret the requested event ID as an integer.

  • Switch back to main.py in Visual Studio Code.
  • Scroll to the end of the list_events() function.
  • Place the detail route below list_events() by pasting this code:
@app.get("/events/{event_id}")
def get_event(event_id: int):
    return events[event_id]

What does this route do?

  • The route stores the number from the URL in event_id.
  • The event_id: int type hint tells FastAPI to parse that number as an integer.
  • The events[event_id] lookup asks Python for the dictionary entry stored under that exact key.
  • Save main.py.
  • Return to http://127.0.0.1:8000/docs in your browser.
  • Refresh the browser page.

Your new detail route is live in the documentation. You should see a GET /events/{event_id} operation beneath the event-list route.

Route missing from the documentation?

  • Confirm that the route sits below list_events() in main.py.
  • Save main.py again so the development server reloads it.

help me troubleshoot why my new FastAPI route is missing from the interactive documentation.

Confirm a known event

The seeded dictionary contains an event under ID 1. This request checks the route under the condition its direct lookup expects.

  • Expand the GET /events/{event_id} operation in the interactive documentation.
  • Start the route's interactive request form.
  • Enter 1 in the event_id field.
  • Submit the request.

You've proved that direct lookup works for a stored ID. The JSON response contains the Python Backend Meetup record with an id of 1.

Known event not returned?

  • Confirm that the event_id field contains 1.
  • Compare the route with the code block above for a missing brace or bracket.

help me debug why event ID 1 is not returned by my FastAPI route.

Expose the missing-event failure

The route currently assumes every requested ID exists in events. An unknown ID tests that assumption at the boundary where a client meets your code.

Before you send the next request, do you think direct dictionary lookup can produce a useful response for an ID that has no matching key?

  • Return to the event_id field.
  • Replace 1 with 999.
  • Submit the request.

You'll see a 500 Internal Server Error response for GET /events/999. The running terminal also shows a traceback from the failed dictionary lookup.

Why the 500 matters

This failure is the intended shortfall. A missing event is a normal client request, but the direct lookup lets it escape as an unhandled server error.

You now have visible evidence that the route needs deliberate error handling. The current behavior gives the client no useful explanation of what went wrong.

✔️ Awesome, I've got everything!

Your detail route returns event ID 1 and exposes the unhandled failure for event ID 999.

ⓧ I'd like to double check the full code

Compare your saved main.py with this complete version.

from fastapi import FastAPI

app = FastAPI()

events: dict[int, dict] = {
    1: {
        "id": 1,
        "name": "Python Backend Meetup",
        "capacity": 2,
        "booked": 0,
    },
    2: {
        "id": 2,
        "name": "API Design Workshop",
        "capacity": 1,
        "booked": 0,
    },
}

bookings: dict[int, dict] = {}


@app.get("/")
def read_root():
    return {"message": "Event Booking API"}


@app.get("/events")
def list_events():
    return list(events.values())


@app.get("/events/{event_id}")
def get_event(event_id: int):
    return events[event_id]

What changed in this file?

The existing application state and list routes remain unchanged. The new get_event() function maps a numeric URL value directly to a key in events.

The missing-event gap is now visible. Next, you'll replace this accidental failure with stable client-facing behavior.

Add Validated Bookings and Errors

A direct dictionary lookup gave you a useful event detail route. The missing-ID test also exposed an unhandled failure in your FastAPI app.

This step turns that failure into an explicit API contract. Typed models validate booking data before your route logic runs.

In this step, get ready to:
  • Define typed event and booking models.
  • Replace the missing-event crash with a deliberate 404 response.
  • Create validated bookings with capacity protection.
Define typed API models

Pydantic models define the fields accepted or returned by a route. FastAPI uses these definitions to validate data automatically.

  • In main.py, select the current import through app = FastAPI().
  • Replace the selected lines with this model setup:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class Event(BaseModel):
    id: int
    name: str
    capacity: int
    booked: int


class BookingCreate(BaseModel):
    event_id: int
    attendee_name: str
    attendee_email: str


class Booking(BookingCreate):
    id: int

What do these models define?

  • The Event model describes every event returned by the API.
  • The BookingCreate model describes the required booking request fields.
  • The Booking model inherits the request fields. It adds the generated booking ID.
  • The HTTPException import lets route logic return deliberate client errors.
  • Save main.py.

The development server reloads the file. You should see fresh reload activity without a Python traceback.

Seeing a reload error?

Check that each class begins at the left edge of main.py. Confirm that every field is indented by four spaces.

Compare both import lines with the code above. A missing import prevents the application from loading these models.

Help me fix the model definitions in main.py.

A response_model states the successful response shape for a route. The interactive documentation uses that contract to describe the returned data.

  • Find the existing list_events route in main.py.
  • Replace that route with this version:
@app.get("/events", response_model=list[Event])
def list_events():
    return list(events.values())

What does the response model add?

The list[Event] contract describes an array of event-shaped objects. FastAPI can now document each returned field.

  • Save main.py.
  • Return to the interactive documentation from earlier.
  • Refresh http://127.0.0.1:8000/docs.
  • Expand GET /events.
  • Inspect the documented successful response schema.

You should see an array of event objects. Each object includes id, name, capacity, and booked.

Response schema missing?

Confirm that the decorator contains response_model=list[Event]. Refresh the documentation after the development server finishes reloading.

Help me find why the event response schema is missing.

Replace accidental failures with deliberate errors

The direct lookup assumes every requested event exists. A safe lookup can detect a missing event before it becomes an internal server failure.

  • Find the existing get_event route in main.py.
  • Replace that route with this version:
@app.get("/events/{event_id}", response_model=Event)
def get_event(event_id: int):
    event = events.get(event_id)
    if event is None:
        raise HTTPException(status_code=404, detail="Event not found")
    return event

How does the deliberate error work?

  • The events.get(event_id) lookup returns None when the ID is absent.
  • The HTTPException converts that missing value into a 404 response.
  • The Event not found detail explains the failure to the client.
  • The response_model=Event contract still describes successful requests.
  • Save main.py.
  • Return to the interactive documentation from earlier.
  • Refresh the page.
  • Expand GET /events/{event_id}.
  • Activate the route's request form.
  • Enter 999 for event_id.

Before you submit, predict whether the missing ID still becomes an internal server failure:

  • Submit the request.

You should see status 404 with {"detail": "Event not found"}. The missing event now has a stable client-facing response.

Still seeing a server error?

Confirm that the route uses events.get(event_id). Direct square-bracket access still causes the original failure.

Check that HTTPException appears in the FastAPI import at the top of main.py.

Help me debug the missing-event response.

Create and verify bookings

A successful booking changes your in-memory state. The route must protect that state from missing events or exhausted capacity.

  • Place your cursor below the completed get_event function in main.py.
  • Add the booking creation route with this code:
@app.post("/bookings", response_model=Booking, status_code=201)
def create_booking(booking: BookingCreate):
    event = events.get(booking.event_id)
    if event is None:
        raise HTTPException(status_code=404, detail="Event not found")
    if event["booked"] >= event["capacity"]:
        raise HTTPException(status_code=409, detail="Event is sold out")

    booking_id = max(bookings, default=0) + 1
    saved_booking = {"id": booking_id, **booking.model_dump()}
    bookings[booking_id] = saved_booking
    event["booked"] += 1
    return saved_booking

What does the booking route protect?

  • The BookingCreate parameter makes FastAPI validate the request body before running the function.
  • The first condition returns 404 for a missing event.
  • The capacity condition returns 409 when every seat is booked.
  • The booking_id calculation generates the next in-memory ID.
  • The saved record increments the selected event's booked value.
  • Save main.py.
  • Return to the interactive documentation from earlier.
  • Refresh the page.
  • Expand POST /bookings.
  • Activate the route's request form.
  • Set event_id to 1.
  • Set attendee_name to Ava.
  • Set attendee_email to ava@example.com.

Before you submit, predict whether this complete request body passes validation:

  • Submit the request.

You should see status 201. The response includes booking ID 1.

That is the first complete booking flow working. Your API has validated the request before saving it.

  • Remove the attendee_email field from the request body.
  • Submit the incomplete request.

You should see status 422 because a required field is missing. The request never reaches create_booking.

  • Restore attendee_email with ava@example.com.
  • Change event_id to 999.
  • Submit the request.

You should see status 404 with {"detail": "Event not found"}.

  • Change event_id to 2.
  • Change attendee_name to Noah.
  • Change attendee_email to noah@example.com.
  • Submit the request once.

You should see status 201. This booking fills the only seat at the API Design Workshop.

Before you submit the same request again, predict how the capacity check responds:

  • Submit the same request again.

You should see status 409 with {"detail": "Event is sold out"}.

Booking responses look wrong?

Confirm that the request uses the exact field names event_id, attendee_name, and attendee_email.

Check the capacity condition for event["booked"] >= event["capacity"]. A different comparison changes the sold-out behavior.

Help me debug my booking responses.

A booking-list route makes successful state changes visible. Failed requests never enter the bookings dictionary.

  • Place your cursor at the bottom of main.py.
  • Add the booking-list route with this code:
@app.get("/bookings", response_model=list[Booking])
def list_bookings():
    return list(bookings.values())

What does this route return?

The route returns every saved booking. Its list[Booking] response model describes the shape of each record.

  • Save main.py.
  • Return to the interactive documentation from earlier.
  • Refresh the page.

Why are the earlier bookings gone?

Saving main.py reloads the development server. The in-memory dictionaries return to their seeded state during that reload.

  • Expand POST /bookings.
  • Activate the route's request form.
  • Set event_id to 1.
  • Set attendee_name to Ava.
  • Set attendee_email to ava@example.com.
  • Submit the request.

You should see status 201 for Ava's booking.

  • Change event_id to 2.
  • Change attendee_name to Noah.
  • Change attendee_email to noah@example.com.
  • Submit the request.

You should see status 201 for Noah's booking.

  • Expand GET /bookings.
  • Activate the route's request form.

Before you submit this final check, predict which booking attempts appear in the saved collection:

  • Submit the request.

You should see the successful bookings for Ava and Noah. Each item includes its generated id.

Booking list empty?

Create both bookings again after the latest server reload. Every reload clears records stored only in memory.

Confirm that create_booking assigns saved_booking to bookings[booking_id].

Help me inspect my saved bookings.

✔️ Awesome, I've got everything!

Your typed models and deliberate error responses are working. Double check that main.py is saved before moving on.

ⓧ I'd like to double check the full code

Compare your complete main.py with this target state.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class Event(BaseModel):
    id: int
    name: str
    capacity: int
    booked: int


class BookingCreate(BaseModel):
    event_id: int
    attendee_name: str
    attendee_email: str


class Booking(BookingCreate):
    id: int


events: dict[int, dict] = {
    1: {
        "id": 1,
        "name": "Python Backend Meetup",
        "capacity": 2,
        "booked": 0,
    },
    2: {
        "id": 2,
        "name": "API Design Workshop",
        "capacity": 1,
        "booked": 0,
    },
}

bookings: dict[int, dict] = {}


@app.get("/")
def read_root():
    return {"message": "Event Booking API"}


@app.get("/events", response_model=list[Event])
def list_events():
    return list(events.values())


@app.get("/events/{event_id}", response_model=Event)
def get_event(event_id: int):
    event = events.get(event_id)
    if event is None:
        raise HTTPException(status_code=404, detail="Event not found")
    return event


@app.post("/bookings", response_model=Booking, status_code=201)
def create_booking(booking: BookingCreate):
    event = events.get(booking.event_id)
    if event is None:
        raise HTTPException(status_code=404, detail="Event not found")
    if event["booked"] >= event["capacity"]:
        raise HTTPException(status_code=409, detail="Event is sold out")

    booking_id = max(bookings, default=0) + 1
    saved_booking = {"id": booking_id, **booking.model_dump()}
    bookings[booking_id] = saved_booking
    event["booked"] += 1
    return saved_booking


@app.get("/bookings", response_model=list[Booking])
def list_bookings():
    return list(bookings.values())

This complete file defines the validated booking contract you verified in the interactive documentation.

You have turned accidental failures into predictable API behavior. Next up, you will protect these contracts with automated tests.

Test the API Contract

Your event booking API now returns deliberate HTTP responses for successful requests and failed requests. Manual checks in the interactive documentation prove each response once.

In this step, you will use pytest with FastAPI's TestClient to turn the API contract into five repeatable checks. Each test starts from the same in-memory state.

In this step, get ready to:
  • Create repeatable test state for the in-memory API.
  • Test successful requests plus each deliberate error response.
  • Run the complete test suite and confirm all five tests pass.
Set up isolated test state

A successful booking changes the shared events and bookings dictionaries. The reset_state() helper restores the starting state before every test.

  • Click the New File icon beside event-booking-api in the Visual Studio Code Explorer sidebar.
  • Type test_main.py in the filename field.
  • Press Enter to create the file.
  • Add the shared test setup to test_main.py by pasting this code:
from fastapi.testclient import TestClient

from main import app, bookings, events

client = TestClient(app)


def reset_state():
    bookings.clear()
    for event in events.values():
        event["booked"] = 0

What does this setup do?

  • TestClient sends requests directly to app without using the development server.
  • bookings.clear() removes bookings left behind by earlier tests.
  • event["booked"] = 0 restores every event's available capacity.
  • Save test_main.py with Ctrl+S.
  • Confirm test_main.py is listed beneath main.py in the Explorer sidebar.

Cannot find the test file?

Check that test_main.py appears directly inside the event-booking-api folder. A nested file may not be discovered with the project tests.

Help me locate my test file.

The first test checks the default event collection. It protects the seeded response count that clients currently rely on.

  • Add the event-list test below reset_state() by pasting this code:
def test_list_events():
    reset_state()
    response = client.get("/events")
    assert response.status_code == 200
    assert len(response.json()) == 2

What does this test prove?

  • client.get("/events") calls the event-list route through the test client.
  • response.status_code == 200 checks that the request succeeds.
  • len(response.json()) == 2 confirms that both seeded events are returned.
  • Save test_main.py.
  • Create a second terminal with the plus icon in the Visual Studio Code terminal panel.

Before you run the test, do you expect it to call the live development server or load the app directly?

  • Run the first automated API check with this command:
uv run pytest

What does this command check?

The command discovers test functions in test_main.py. It runs each function whose name starts with test_.

You should see one passing test in the terminal. This proves the test client can load your application and inspect its JSON response.

Does the first test fail?

Check that the second terminal is still inside the event-booking-api folder. Confirm that main.py sits beside test_main.py.

Help me debug the first API test.

Test missing events and successful bookings

An API contract includes the response body as well as the status code. The next tests protect the deliberate missing-event error and the successful booking payload.

  • Add the missing-event test below test_list_events() by pasting this code:
def test_missing_event_returns_404():
    reset_state()
    response = client.get("/events/999")
    assert response.status_code == 404
    assert response.json() == {"detail": "Event not found"}

What does the missing-event test protect?

  • client.get("/events/999") requests an event ID that does not exist.
  • response.status_code == 404 protects the deliberate client-facing status.
  • response.json() confirms that the response explains why the request failed.
  • Save test_main.py.
  • Run the expanded test suite with this command:
uv run pytest

What does this run confirm?

The test runner executes the event-list test plus the new missing-event test. Both contracts must pass in the same run.

You should see two passing tests. The missing resource now has automated protection for its status and error body.

Does the missing-event test fail?

Compare the expected detail text with the HTTPException in get_event(). Capitalization must match the response exactly.

Help me debug the missing-event test.

A successful creation contract covers the generated ID plus every accepted request field. It also confirms that the route uses the creation status.

  • Add the successful-booking test below test_missing_event_returns_404() by pasting this code:
def test_create_booking():
    reset_state()
    response = client.post(
        "/bookings",
        json={
            "event_id": 1,
            "attendee_name": "Ava",
            "attendee_email": "ava@example.com",
        },
    )
    assert response.status_code == 201
    assert response.json() == {
        "event_id": 1,
        "attendee_name": "Ava",
        "attendee_email": "ava@example.com",
        "id": 1,
    }

What does the booking test protect?

  • client.post() sends a complete booking body to the creation route.
  • response.status_code == 201 confirms that the API reports a successful creation.
  • response.json() checks the stored attendee data plus the generated booking ID.
  • Save test_main.py.
  • Run all current tests with this command:
uv run pytest

What does this run confirm?

This run checks the event list plus both detail and creation contracts. The reset helper gives the booking test a fresh generated ID.

You should see three passing tests. Your main successful booking flow is now protected from accidental changes.

Does the booking test fail?

Confirm that the request body includes event_id, attendee_name, and attendee_email with the exact values shown above.

Help me debug the successful booking test.

Test validation and sold-out capacity

The final two tests target requests that clients must handle safely. One protects automatic body validation. The other protects the deliberate capacity conflict.

  • Add the malformed-booking test below test_create_booking() by pasting this code:
def test_invalid_booking_returns_422():
    reset_state()
    response = client.post(
        "/bookings",
        json={"event_id": 1, "attendee_name": "Ava"},
    )
    assert response.status_code == 422

What does the validation test protect?

  • The request omits attendee_email from the booking body.
  • response.status_code == 422 confirms that malformed data is rejected before the route creates a booking.
  • Save test_main.py.
  • Run the validation test with the existing suite by using this command:
uv run pytest

What does this run confirm?

The test runner now checks the automatic validation contract alongside the existing route behavior. A malformed request must stay separate from valid booking creation.

You should see four passing tests. The required booking fields now have an automated guard.

Does the validation test fail?

Check that the test body leaves out only attendee_email. Confirm that BookingCreate still declares that field as str.

Help me debug the malformed booking test.

The sold-out test creates its own capacity condition before sending a booking. This keeps the test independent from any order chosen by the test runner.

  • Add the sold-out event test below test_invalid_booking_returns_422() by pasting this code:
def test_sold_out_event_returns_409():
    reset_state()
    events[2]["booked"] = events[2]["capacity"]
    response = client.post(
        "/bookings",
        json={
            "event_id": 2,
            "attendee_name": "Noah",
            "attendee_email": "noah@example.com",
        },
    )
    assert response.status_code == 409
    assert response.json() == {"detail": "Event is sold out"}

What does the capacity test protect?

  • events[2]["booked"] = events[2]["capacity"] makes the workshop sold out for this test.
  • response.status_code == 409 protects the deliberate capacity conflict response.
  • response.json() confirms that the client receives the sold-out explanation.
  • Save test_main.py.

Before you run the complete suite, do you expect any test to inherit a booking or sold-out event from an earlier test?

  • Run all five API contract tests with this command:
uv run pytest

What does the final test run prove?

The complete suite covers event listing, missing events, successful booking creation, malformed booking validation, and sold-out capacity. The reset helper isolates every result.

You should see 5 passed in the terminal. Every success path and error path in this project's API contract now has an automated check.

Do fewer than five tests pass?

Confirm that all five function names begin with test_. Check that each function starts at the left edge of test_main.py.

Help me debug the complete test suite.

That is your safety net in place. Your five tests can now catch changes that break the API's successful responses or deliberate errors.

✔️ Awesome, I've got everything!

Great work. Double-check that test_main.py is saved and the terminal reports 5 passed.

ⓧ I'd like to double check the full code

Compare your complete test_main.py with this reference. The order keeps the setup first and groups the five API contracts beneath it.

from fastapi.testclient import TestClient

from main import app, bookings, events

client = TestClient(app)


def reset_state():
    bookings.clear()
    for event in events.values():
        event["booked"] = 0


def test_list_events():
    reset_state()
    response = client.get("/events")
    assert response.status_code == 200
    assert len(response.json()) == 2


def test_missing_event_returns_404():
    reset_state()
    response = client.get("/events/999")
    assert response.status_code == 404
    assert response.json() == {"detail": "Event not found"}


def test_create_booking():
    reset_state()
    response = client.post(
        "/bookings",
        json={
            "event_id": 1,
            "attendee_name": "Ava",
            "attendee_email": "ava@example.com",
        },
    )
    assert response.status_code == 201
    assert response.json() == {
        "event_id": 1,
        "attendee_name": "Ava",
        "attendee_email": "ava@example.com",
        "id": 1,
    }


def test_invalid_booking_returns_422():
    reset_state()
    response = client.post(
        "/bookings",
        json={"event_id": 1, "attendee_name": "Ava"},
    )
    assert response.status_code == 422


def test_sold_out_event_returns_409():
    reset_state()
    events[2]["booked"] = events[2]["capacity"]
    response = client.post(
        "/bookings",
        json={
            "event_id": 2,
            "attendee_name": "Noah",
            "attendee_email": "noah@example.com",
        },
    )
    assert response.status_code == 409
    assert response.json() == {"detail": "Event is sold out"}

How is the full test file organized?

The file creates one shared test client and one reset helper. Five independent test functions then protect the API's current success and error behavior.

Secret mission

Filter Events with Available Seats

Clients need a quick way to hide events that have no seats left. Add a typed availability filter that returns bookable events while preserving the complete event list by default.

Clean Up Your Resources

Clean Up Your Resources

Everything in this project runs locally, so there are no ongoing costs. Choose whether to keep the API available, pause the server, or delete the project entirely.

Resources you used:

  • The complete event-booking-api folder.
  • Project files inside event-booking-api: pyproject.toml, main.py, and test_main.py.
  • Generated resources inside event-booking-api: .venv and uv.lock.
  • The local FastAPI development server serving documentation at http://127.0.0.1:8000/docs.

Keep everything running

No action is needed. Choose this option if you want to keep testing the API or extending its routes.

  • Leave the development server running if you want the interactive documentation to stay available.
  • Keep the event-booking-api folder so its source files remain available.
  • Keep .venv and uv.lock so the pinned environment remains reproducible.

Pause - I'll come back to this later

Stop the local server to release its terminal and port. Your code, environment, and lockfile remain ready for another session.

  • Return to the Visual Studio Code terminal from earlier.
  • Press Ctrl+C if the development server is running.

Your event-booking-api folder remains unchanged after the server stops.

  • Restart the development server later from the terminal in event-booking-api by running:
uv run fastapi dev

What does this command do?

The command starts the local development server using the environment managed for this project.

It also enables automatic reload while you edit the API.

  • Visit http://127.0.0.1:8000/docs in your browser.

You'll see the interactive documentation with the event and booking routes.

Delete - I don't want to use this again

Deleting the folder permanently removes your code, environment, and lockfile from this computer. No cloud resources or paid services are attached to it.

  • Return to the Visual Studio Code terminal from earlier.
  • Press Ctrl+C if the development server is running.
  • Delete the event-booking-api folder by running the commands for your operating system below:

macOS

cd ..
rm -rf event-booking-api
What do these commands do?

The first line moves the terminal out of the project folder.

The second line permanently removes the project folder and everything inside it.

Windows

cd ..
rmdir /s /q event-booking-api
What do these commands do?

The first line moves the terminal out of the project folder.

The second line permanently removes the project folder and everything inside it.

  • Check the folder that previously contained event-booking-api in Finder on macOS or File Explorer on Windows.

You should no longer see the event-booking-api folder.

Nice Work!

Nice Work!

Project complete! Your local FastAPI backend now powers a tested event booking flow from browsing events through reserving available seats.

You've learned how to:

  • Build an event list route backed by in-memory state. Retrieve one event through a numeric path parameter. Create bookings that reduce the selected event's remaining capacity.
  • Define typed API contracts with Pydantic request models. Reject malformed booking data automatically with status 422. Return a deliberate 404 for missing events. Return a deliberate 409 for sold-out events. Confirm successful booking creation with 201.
  • Protect the API contract with six automated tests. Use TestClient to exercise successful requests plus error paths. Reset in-memory state so every pytest test starts from the same event data.
  • Secret Mission: Added a typed availability query parameter through available_only. Confirmed that clients can hide sold-out events while the default event list still returns every event.

Ready to quiz yourself?