Build a Movie Ticket Booking API

Build a REST API with FastAPI that handles bookings and prevents double-booking.

Introduction

30 Second Summary

Every time you book a movie ticket online, a backend system is checking which seats are free, reserving your pick, and making sure nobody else can grab it at the same time.

In this project, you will build that backend system yourself. You'll create a REST API that manages movies, showtimes, and ticket bookings, complete with seat tracking and double-booking prevention.

What You'll Build

By the end, you'll have a running API you can demo live in the browser: pick a movie, choose a seat, book it, and watch the API reject anyone who tries to take that same seat.

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

  • A movie catalog endpoint that returns movie details like title, genre, and duration as structured JSON.
  • A showtime and seat map that shows real-time availability for every seat in a screening.
  • A booking system that reserves a seat, returns a unique confirmation ID, and rejects double-bookings with a proper 409 Conflict error.
  • Auto-generated interactive API documentation where you can test every endpoint live in the browser.
  • Secret Mission: Add a booking cancellation endpoint that releases the seat back to available.

Are there any prerequisites?

You should be comfortable with basic Python (variables, functions, dictionaries, and lists) and know how to run commands in a terminal.

No prior experience with web frameworks or APIs is needed. This project teaches those concepts as you build.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Getting clear on the end goal now helps you connect every step to the bigger picture of your backend development career.

Set Up Your Project and Install FastAPI

You need a working development environment before you can build anything. Getting the server running immediately proves your tools work and gives you a live playground for the rest of the project.

In this step, you'll create a project folder, set up a Python virtual environment to keep your dependencies isolated, and install FastAPI. By the end, you'll have a running server you can visit in your browser.

In this step, get ready to:
  • Create a project folder and Python virtual environment.
  • Install FastAPI and create your first endpoint.
  • Start the development server and view the interactive docs.
Create your project folder and virtual environment

A virtual environment is an isolated space where Python installs packages just for this project. It keeps your system clean and prevents conflicts between projects.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open a terminal inside VS Code by pressing Ctrl+` (the backtick key, below Esc on most keyboards).

Can't find the backtick key?

You can also open the terminal from the menu bar: click Terminal, then New Terminal.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir movie-ticket-api
cd movie-ticket-api
  • Now tell VS Code to use this folder as your workspace. Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the movie-ticket-api folder, and click Open.

Why reopen the folder?

Opening a folder as your workspace tells VS Code to treat it as a project. The Explorer sidebar shows your project files, and any new terminal opens inside this folder automatically.

VS Code reloads with your project folder as the workspace. You should see movie-ticket-api in the Explorer sidebar on the left.

  • If VS Code asks whether you trust the authors of this folder, click Yes, I trust the authors.
  • Open a new terminal inside VS Code by pressing Ctrl+`.

Now create and activate your virtual environment. The commands differ by operating system.

macOS

  • Create your virtual environment by running:
python3 -m venv venv
  • Activate the virtual environment by running:
source venv/bin/activate

Windows

  • Create your virtual environment by running:
python -m venv venv
  • Activate the virtual environment by running:
venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms you are working inside the virtual environment.

Don't see (venv) in your prompt?

On Windows, you might see an error about execution policies. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try activating again.

On macOS, make sure you typed source before the path. Without it, the command won't work.

Help me activate my Python virtual environment.

Install FastAPI and create your first endpoint
  • Install FastAPI and all its standard dependencies by running:
pip install "fastapi[standard]"

You'll see pip downloading and installing several packages. When it finishes, you'll see a "Successfully installed" message listing everything it added.

Seeing a permission error or "pip not found"?

Make sure (venv) is visible in your prompt. If it is not, go back and activate the virtual environment first. The pip command only works reliably inside an active virtual environment.

Help me fix my pip install issue.

Now create the file that will hold your API code.

  • In the VS Code Explorer sidebar on the left, hover over the movie-ticket-api folder name.
  • Click the New File icon (a page with a plus sign).
  • Type main.py and press Enter.
  • Paste the following code into main.py:
from fastapi import FastAPI

# Create the FastAPI application
app = FastAPI(title="Movie Ticket Booking API")

# Define a root endpoint that returns a greeting
@app.get("/")
def read_root():
    return {"message": "Hello, World!"}

What does this code do?

  • from fastapi import FastAPI imports the framework you just installed.
  • app = FastAPI(title="Movie Ticket Booking API") creates your application instance. The title shows up in the auto-generated docs.
  • @app.get("/") tells FastAPI to respond to GET requests at the root URL.
  • def read_root() is the function that runs when someone visits that URL. It returns a JSON object automatically.
  • Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

Here is the complete main.py file so far:

from fastapi import FastAPI

app = FastAPI(title="Movie Ticket Booking API")

@app.get("/")
def read_root():
    return {"message": "Hello, World!"}
Start the development server

Your code is written and saved. Time to see it in action.

  • Make sure your terminal still shows (venv) in the prompt.

Before you run the next command, what do you think will happen? The server should start and tell you where to find your API.

  • Start the FastAPI development server by running:
fastapi dev main.py

You should see output that includes a line like Serving at: http://127.0.0.1:8000. The server is now running and waiting for requests.

What does this command do?

fastapi dev starts a development server with auto-reload enabled. Every time you save main.py, the server automatically picks up your changes without needing a manual restart.

The server takes over this terminal while it runs. You'll open a new terminal later when you need to run other commands.

  • Open your browser and go to http://127.0.0.1:8000.

You should see a JSON response: {"message":"Hello, World!"}. Your API is alive.

  • Now visit http://127.0.0.1:8000/docs in your browser.

This is the auto-generated interactive documentation page. FastAPI builds this for free from your code. You'll use it throughout the project to test every endpoint without needing a separate tool.

Seeing an error when starting the server?

If you see command not found: fastapi, your virtual environment is not active. Check that (venv) is in your prompt, then try the command again.

If you see an import error, make sure you saved main.py in the movie-ticket-api folder (not inside the venv folder).

Help me get my FastAPI server running.

Your development environment is ready and your server is running. Next up, you'll build the movie listings endpoint so your API has real data to serve.

Build the Movie Listings Endpoint

Your server is running and responding to requests. Now it's time to build something useful on top of it.

Before anyone can book tickets, they need movies to browse. In this step, you'll define a data model, populate it with movies, and create endpoints that serve structured JSON to any client that asks.

In this step, get ready to:
  • Define a Movie data model using Pydantic.
  • Create endpoints to list all movies and fetch a single movie by ID.
  • Handle the case where a requested movie does not exist.
Define the Movie model

Every API needs a clear contract for the shape of its data. Pydantic models define that shape, and FastAPI uses them to automatically validate data and generate documentation.

  • In VS Code, open main.py and replace its entire content with the following code:
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="Movie Ticket Booking API")

# --- Data Models ---

class Movie(BaseModel):
    id: int
    title: str
    genre: str
    duration_minutes: int

What does this code do?

  • from pydantic import BaseModel imports Pydantic's base class for data models. FastAPI uses Pydantic under the hood to validate request and response data.
  • class Movie(BaseModel) declares a model with four typed fields. Any data that doesn't match these types (like passing a string for id) gets rejected automatically.
  • The old read_root endpoint is gone. You'll replace it with proper movie endpoints next.
  • Save main.py.
  • Check your terminal. The dev server auto-reloads and you should see a fresh startup message with no errors.

Seeing an import error?

  • Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
  • Confirm FastAPI is installed by running pip list and checking that fastapi and pydantic both appear.

Why isn't my FastAPI server reloading after I save?

Add the movie data

With the model defined, you need some actual movies to serve. For now, you'll store them in a simple Python list. This keeps the project focused on API concepts without needing a database.

  • In main.py, add the following code below the Movie class:
# --- In-Memory Data ---

movies: list[dict] = [
    {"id": 1, "title": "The Matrix", "genre": "Sci-Fi", "duration_minutes": 136},
    {"id": 2, "title": "Inception", "genre": "Sci-Fi", "duration_minutes": 148},
    {"id": 3, "title": "The Dark Knight", "genre": "Action", "duration_minutes": 152},
]

What does this code do?

movies: list[dict] creates a list of three movie dictionaries. Each dictionary has the same fields as the Movie model. When FastAPI returns this data, Pydantic automatically validates each dictionary against the model's type annotations.

In a production app, this data would come from a database. Storing it in memory lets you focus on routing and validation without extra setup.

  • Save main.py.
  • Check the terminal again. The server reloads with no errors.
Create the movie endpoints

You have a model and data. Now you need endpoints that actually serve this data to clients over HTTP.

  • In main.py, add the following code below the movies list:
# --- Movie Endpoints ---

@app.get("/movies", response_model=list[Movie])
def get_movies():
    return movies

@app.get("/movies/{movie_id}", response_model=Movie)
def get_movie(movie_id: int):
    for movie in movies:
        if movie["id"] == movie_id:
            return movie

What does this code do?

  • @app.get("/movies", response_model=list[Movie]) registers a GET endpoint at /movies. The response_model tells FastAPI to validate the output and document it in the interactive docs.
  • @app.get("/movies/{movie_id}") uses a path parameter. FastAPI automatically converts movie_id from the URL string to an int because of the type annotation.
  • The loop searches the movies list for a matching ID. If found, it returns that movie. If no match is found, the function ends without returning anything.
  • Save main.py.
  • Open your browser and go to http://127.0.0.1:8000/docs.
  • You should see two new endpoints listed: GET /movies and GET /movies/{movie_id}.

Don't see the new endpoints in the docs?

  • Hard-refresh the docs page with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).
  • Check that you saved main.py and that the terminal shows a successful reload.
  • Make sure your new code is below the movies list, not inside the Movie class.

Why aren't my new endpoints showing up in the FastAPI docs?

Test the endpoints and fix the missing movie problem
  • In the docs page, expand the GET /movies section.
  • Click Try it out, then click Execute.

You should see a JSON array with three movies, each showing their id, title, genre, and duration_minutes.

  • Now expand GET /movies/{movie_id}.
  • Click Try it out, enter 1 for movie_id, and click Execute.

You should see The Matrix returned as a single JSON object with a 200 status code.

Now here's the interesting part. What do you think will happen if you request a movie that doesn't exist?

  • Enter 99 for movie_id and click Execute.

Instead of a helpful error message, you get back null with a 200 OK status. The API pretends everything is fine when it clearly is not. A real client would have no idea the movie doesn't exist.

Why is this a problem?

When no movie matches the ID, the function ends without returning anything. Python implicitly returns None, which FastAPI serializes as null. The status code stays 200, so the client thinks the request succeeded.

A proper REST API should return a 404 Not Found status with a clear error message. You'll fix this now.

  • In main.py, find the import line at the top:
from fastapi import FastAPI
  • Replace it with this updated import that adds HTTPException:
from fastapi import FastAPI, HTTPException
  • Now find the get_movie function and add a raise HTTPException line at the end, after the loop. The complete function should look like this:
@app.get("/movies/{movie_id}", response_model=Movie)
def get_movie(movie_id: int):
    for movie in movies:
        if movie["id"] == movie_id:
            return movie
    raise HTTPException(status_code=404, detail="Movie not found")

What does HTTPException do?

raise HTTPException(status_code=404, detail="Movie not found") immediately stops the function and sends a 404 response to the client. The detail message appears in the JSON body so the client knows exactly what went wrong.

This line only runs if the loop finishes without finding a match. If a movie IS found, the return inside the loop exits the function before reaching the raise.

  • Save main.py.
  • Back in the docs, request movie ID 99 again.

This time you should see a 404 response with the body {"detail": "Movie not found"}. That's the correct behaviour for a missing resource.

Still getting null or a 200 status?

  • Make sure the raise HTTPException line is OUTSIDE the for loop (same indentation as the for keyword itself), not inside it.
  • Confirm you updated the import at the top to include HTTPException.
  • Check the terminal for any reload errors.

Why is my HTTPException not working in FastAPI?

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and that both endpoints work in the docs before moving on.

ⓧ I'd like to double check the full code

Here is the complete main.py at this point:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Movie Ticket Booking API")

# --- Data Models ---

class Movie(BaseModel):
    id: int
    title: str
    genre: str
    duration_minutes: int

# --- In-Memory Data ---

movies: list[dict] = [
    {"id": 1, "title": "The Matrix", "genre": "Sci-Fi", "duration_minutes": 136},
    {"id": 2, "title": "Inception", "genre": "Sci-Fi", "duration_minutes": 148},
    {"id": 3, "title": "The Dark Knight", "genre": "Action", "duration_minutes": 152},
]
# --- Movie Endpoints ---

@app.get("/movies", response_model=list[Movie])
def get_movies():
    return movies

@app.get("/movies/{movie_id}", response_model=Movie)
def get_movie(movie_id: int):
    for movie in movies:
        if movie["id"] == movie_id:
            return movie
    raise HTTPException(status_code=404, detail="Movie not found")

Your API now serves movie data and handles missing resources properly. Next up, you'll add showtimes with seat maps so there's actually something to book.

Add Showtimes with Seat Availability

Your movie listings endpoint is live and returning structured data. But knowing a movie exists is not the same as knowing when you can watch it or where you can sit.

In this step, you will add showtimes that link to your movies and include a seat map. Each seat tracks whether it is available or already taken. This is the data layer that makes booking possible.

In this step, get ready to:
  • Define a Showtime data model with a seat availability map.
  • Create sample showtime data with 10 seats per screening.
  • Build endpoints that return showtimes and their seat maps.
Define the Showtime model

A showtime represents a specific screening of a movie at a specific time. It needs to track which movie is playing, when, and which seats are available.

  • In main.py, find the Movie class and add the following Showtime model directly below it:
class Showtime(BaseModel):
    id: int
    movie_id: int
    date_time: str
    seats: dict[str, bool]

What does this model do?

This Pydantic model defines the shape of a showtime object.

  • movie_id links the showtime to a specific movie from your movies list.
  • seats is a dictionary where keys are seat labels (like A1) and values are booleans. True means available, False means booked.
  • Save main.py.
  • Check your terminal where the dev server is running. You should see it reload without any errors.

Seeing an indentation or syntax error?

  • Make sure the Showtime class is at the same indentation level as the Movie class above it.
  • Confirm there is a blank line between the end of Movie and the start of Showtime.
  • Check that dict[str, bool] uses square brackets, not parentheses.

Still stuck? Help me fix this Pydantic model error in my FastAPI app.

Create the showtime data

You need sample showtimes to serve from your endpoints. Each showtime links to a movie and carries a seat map with 10 seats arranged in two rows.

  • In main.py, find the movies list. Add the following showtimes list directly below it:
showtimes: list[dict] = [
    {
        "id": 1,
        "movie_id": 1,
        "date_time": "2026-07-25 19:00",
        "seats": {f"{row}{num}": True for row in "AB" for num in range(1, 6)},
    },
    {
        "id": 2,
        "movie_id": 2,
        "date_time": "2026-07-25 21:00",
        "seats": {f"{row}{num}": True for row in "AB" for num in range(1, 6)},
    },
]

What does this data do?

Each showtime dictionary links to a movie via movie_id and includes a screening time.

The seats dictionary is built with a dictionary comprehension. It generates 10 seats: A1 through A5 (row A) and B1 through B5 (row B). Every seat starts as True (available).

  • Save main.py.
  • Check your terminal again. The server should reload successfully with no errors.

Server showing a syntax error?

  • The dictionary comprehension uses an f-string: f"{row}{num}". Make sure the outer braces belong to the dict comprehension and the inner braces are the f-string placeholders.
  • Confirm the showtimes list is placed after the closing bracket of the movies list, not inside it.

Need help? Help me fix this dictionary comprehension syntax error in my showtimes data.

Create the showtime endpoints

The data exists but is not accessible through the API yet. You need a helper function to look up showtimes by ID, and two endpoints to expose the data.

  • In main.py, add the following helper function between the data section and the movie endpoints. Place it with a new section comment:
# --- Helper Functions ---

def find_showtime(showtime_id: int) -> dict | None:
    for showtime in showtimes:
        if showtime["id"] == showtime_id:
            return showtime
    return None

What does this helper do?

find_showtime loops through the showtimes list and returns the matching dictionary. If no showtime has the given ID, it returns None. This keeps your endpoint code clean by extracting the lookup logic into one reusable place.

  • Now add the showtime endpoints at the bottom of main.py, below the movie endpoints:
# --- Showtime Endpoints ---

@app.get("/showtimes", response_model=list[Showtime])
def get_showtimes():
    return showtimes

@app.get("/showtimes/{showtime_id}", response_model=Showtime)
def get_showtime(showtime_id: int):
    showtime = find_showtime(showtime_id)
    if not showtime:
        raise HTTPException(status_code=404, detail="Showtime not found")
    return showtime

What do these endpoints do?

  • GET /showtimes returns the full list of showtimes, including their seat maps.
  • GET /showtimes/{showtime_id} returns a single showtime by ID. If the ID does not exist, it raises a 404 error with a descriptive message.
  • response_model=list[Showtime] tells FastAPI to validate the output against your Showtime model and document it in the interactive docs.
  • Save main.py.

Before you test, what do you expect to see when you request a single showtime? Think about what the seats field should look like.

  • Switch to your browser and open http://127.0.0.1:8000/docs.
  • Find the GET /showtimes/{showtime_id} endpoint and click it to expand.
  • Click Try it out.
  • Enter 1 in the showtime_id field.
  • Click Execute.

You should see a JSON response with a seats dictionary showing seat labels (A1, A2, through B5) and their availability status (all true since no one has booked yet).

Not seeing the showtime endpoints in the docs?

  • Refresh the docs page. The browser may be showing a cached version from before you added the new endpoints.
  • Check your terminal for errors. If the server did not reload cleanly, your new code has a syntax issue.
  • Confirm the endpoint functions are at the top level of the file (no accidental indentation under another function).

Still stuck? Help me debug why my new FastAPI endpoints are not showing up in the docs page.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and your server is still running.

ⓧ I'd like to double check the full code

Your main.py is now over 30 lines. Scroll through your file and confirm you have each of these sections in order:

  • Imports (FastAPI, HTTPException, BaseModel).
  • App instance: app = FastAPI(title="Movie Ticket Booking API").
  • Data Models section with Movie and Showtime classes.
  • In-Memory Data section with the movies list followed by the showtimes list.
  • Helper Functions section with find_showtime.
  • Movie Endpoints section with get_movies and get_movie.
  • Showtime Endpoints section with get_showtimes and get_showtime.

Your API now knows what movies are playing, when, and which seats are open. Next up, you will build the booking endpoint that actually lets someone claim a seat.

Create the Booking Endpoint

Your API can list movies and show seat maps, but there is no way for a customer to actually reserve a seat.

This step builds the core business logic of the whole API: a booking endpoint that takes a showtime and seat, marks that seat as unavailable, and hands back a confirmation. When it works, you will see a seat flip from available to taken in real time.

In this step, get ready to:
  • Define request and response data models for bookings.
  • Create a POST /bookings endpoint that validates input and reserves a seat.
  • Test the full booking flow and confirm a seat becomes unavailable.
Define the booking data models

The booking endpoint needs two Pydantic models: one describing what the client sends in (the request), and one describing what the API sends back (the response).

  • In main.py, find the imports at the very top of the file:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
  • Replace those two import lines with the following three lines:
from uuid import uuid4

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel

What changed in the imports?

You added uuid4 from Python's standard library to generate unique booking IDs.

You also added status from FastAPI, which provides named constants like HTTP_201_CREATED instead of raw numbers.

  • Scroll down to the Showtime model (the last class in the Data Models section). Add the following two models directly below it:
class BookingRequest(BaseModel):
    showtime_id: int
    seat_label: str
    customer_name: str

class BookingResponse(BaseModel):
    booking_id: str
    showtime_id: int
    seat_label: str
    customer_name: str
    status: str

What do these models do?

BookingRequest defines what the client must send to create a booking: which showtime, which seat, and who is booking.

BookingResponse defines what the API sends back: the booking details plus a unique booking_id and a status field. FastAPI uses these models to auto-validate requests and auto-document responses.

  • Scroll down to the In-Memory Data section. Find the closing ] of the showtimes list. Add this line directly below it:
bookings: list[dict] = []
  • Save main.py.
  • Check your terminal where the dev server is running. You should see it reload without any errors.

Seeing an import error or IndentationError?

  • Make sure from uuid import uuid4 is on the very first line of the file, with a blank line between it and the from fastapi import.
  • Check that each model class is indented consistently (4 spaces per level).
  • Confirm status appears after HTTPException in the from fastapi import line, separated by a comma.

Help me fix my FastAPI import or model error

Create the POST /bookings endpoint

The models define the shape of data going in and out. Now you need the actual endpoint that receives a booking request, checks it is valid, and reserves the seat.

  • At the very bottom of main.py, add a new comment header and the booking endpoint by pasting the following code:
# --- Booking Endpoints ---

@app.post("/bookings", response_model=BookingResponse, status_code=status.HTTP_201_CREATED)
def create_booking(request: BookingRequest):
    # Check that the showtime exists
    showtime = find_showtime(request.showtime_id)
    if not showtime:
        raise HTTPException(status_code=404, detail="Showtime not found")

    # Check that the seat label is valid
    if request.seat_label not in showtime["seats"]:
        raise HTTPException(status_code=400, detail=f"Seat {request.seat_label} does not exist for this showtime")

    # Mark the seat as unavailable
    showtime["seats"][request.seat_label] = False

What does this code do?

  • The decorator @app.post creates a POST endpoint. The status_code=status.HTTP_201_CREATED tells the API to return a 201 status on success (the standard for "resource created").
  • find_showtime(request.showtime_id) reuses the helper you already wrote to look up the showtime. If it does not exist, the API returns a 404.
  • The seat-existence check prevents requests with typos like "Z9" from silently passing through.
  • Setting the seat to False in the dictionary is the actual reservation. The in-memory data updates immediately.
  • Directly below that code (still inside the create_booking function), add the booking creation logic:
    # Create the booking record
    booking = {
        "booking_id": str(uuid4()),
        "showtime_id": request.showtime_id,
        "seat_label": request.seat_label,
        "customer_name": request.customer_name,
        "status": "confirmed",
    }
    bookings.append(booking)

    return booking

What does this code do?

  • str(uuid4()) generates a unique ID string like "a1b2c3d4-e5f6-..." so every booking has its own identifier.
  • The booking dictionary is appended to the in-memory bookings list so the API remembers it.
  • Returning the dictionary works because FastAPI automatically validates it against BookingResponse (the response_model in the decorator).
  • Save main.py.
  • Check your terminal. The server should reload and you should see no errors.
  • Open http://127.0.0.1:8000/docs in your browser and confirm you see a new green POST /bookings section in the endpoint list.

Don't see POST /bookings in the docs?

  • Make sure the entire create_booking function body is indented one level inside the function definition.
  • Confirm you pasted the second code block (the booking creation logic) inside the same function, not at the module level.
  • Hard-refresh the docs page with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear cached HTML.

Help me fix my POST /bookings endpoint

Test the full booking flow

The endpoint exists, but does it actually flip a seat from available to unavailable? Time to find out.

  • In the docs page, expand the GET /showtimes/{showtime_id} section.
  • Click Try it out.
  • Enter 1 for showtime_id and click Execute.
  • Look at the seats in the response. Every seat should show true (all available).

Now book a seat and see what changes.

  • Expand the POST /bookings section.
  • Click Try it out.
  • Replace the example request body with this JSON:
{
  "showtime_id": 1,
  "seat_label": "A1",
  "customer_name": "Alice"
}

Before you click Execute, predict: what HTTP status code do you think the API will return for a successful booking?

  • Click Execute.

You should see a 201 response with a JSON body containing a unique booking_id, the showtime and seat you chose, your customer name, and a status of "confirmed".

  • Now go back to GET /showtimes/{showtime_id}.
  • Click Try it out, enter 1 again, and click Execute.
  • Find seat A1 in the response. It should now show false while every other seat remains true.

That single flip from true to false is proof your booking logic is working. A customer requested a seat, and the system reserved it.

Seat still shows true after booking?

  • Confirm the POST /bookings response returned status code 201. If you see 404 or 400, the booking did not go through.
  • Make sure you used "showtime_id": 1 (an integer) and "seat_label": "A1" (uppercase A, number 1).
  • Check that the line showtime["seats"][request.seat_label] = False exists in your create_booking function before the booking dictionary is created.

Help me debug my booking endpoint

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and your server is still running.

ⓧ I'd like to double check the full code

Scroll up through your main.py and confirm each section is present in this order:

  • Imports: from uuid import uuid4, then from fastapi import FastAPI, HTTPException, status, then from pydantic import BaseModel.
  • Data Models: Movie, Showtime, BookingRequest, BookingResponse.
  • In-Memory Data: movies list, showtimes list, bookings: list[dict] = [].
  • Helper Functions: find_showtime.
  • Movie Endpoints: GET /movies and GET /movies/{movie_id}.
  • Showtime Endpoints: GET /showtimes and GET /showtimes/{showtime_id}.
  • Booking Endpoints: POST /bookings with the create_booking function.

Your booking endpoint works. Customers can reserve seats and the system tracks availability in real time. But here is a question: what happens if two people try to book the same seat? Next up, you will handle that conflict.

Handle the Double-Booking Problem

Your booking endpoint works. You can book a seat for a showtime and get a confirmation back.

But there is a critical bug hiding in the code. Nothing stops someone from booking the same seat twice. If two customers both get a confirmation for seat A1, one of them shows up to a taken seat. This is the kind of bug that would cost a real cinema money.

In this step, get ready to:
  • Add double-booking protection that rejects duplicate seat bookings with a 409 Conflict error.
  • Add endpoints to retrieve booking records.
  • Test all error scenarios in the interactive docs.
Expose the double-booking bug

Before you fix anything, see the problem for yourself. Your current booking endpoint has no protection against booking the same seat twice.

  • Switch back to the interactive docs at http://127.0.0.1:8000/docs in your browser.
  • Expand the POST /bookings endpoint and click Try it out.
  • Enter this request body and click Execute:
{
  "showtime_id": 1,
  "seat_label": "A1",
  "customer_name": "Alice"
}
  • You should see a 201 response with a booking confirmation. Seat A1 is now booked.
  • Now click Execute again with the same body. Before you do, predict what will happen. Should the API allow booking the same seat a second time?

You get another 201 success. The API happily books seat A1 again for a second customer, creating a duplicate booking with no warning. That is the bug.

Why is this a problem?

The seat was already set to False (unavailable) after the first booking, but the code never checks that value before overwriting it. It just sets it to False again and creates a duplicate booking record.

A real cinema needs the API to reject the second attempt with a clear error message so the customer knows to pick a different seat.

Add the 409 conflict check

The fix is a single check: before marking a seat as booked, verify it is still available. If it is already taken, raise an HTTPException with status code 409 (Conflict).

  • In main.py, find the create_booking function. Locate these two lines inside it:
    if request.seat_label not in showtime["seats"]:
        raise HTTPException(status_code=400, detail=f"Seat {request.seat_label} does not exist for this showtime")
  • Directly below that check, add this new validation block:
    if not showtime["seats"][request.seat_label]:
        raise HTTPException(status_code=409, detail=f"Seat {request.seat_label} is already booked for this showtime")

What does this code do?

The seats dictionary maps seat labels to booleans. True means available, False means taken.

If showtime["seats"][request.seat_label] is False, the seat is already booked. The not flips that to True, entering the if block and raising a 409 Conflict error.

  • Save main.py. The dev server auto-reloads, which resets the in-memory data (all seats become available again).
  • Return to http://127.0.0.1:8000/docs and test the fix. Book seat A1 for showtime 1 (same body as before). You should get a 201 success.
  • Now execute the same request again. What do you predict this time?

You should see a 409 response with the detail message: Seat A1 is already booked for this showtime. The double-booking bug is fixed.

Still seeing 201 on the second booking?

  • Make sure you saved main.py after adding the new check. The dev server only reloads when the file is saved.
  • Verify the new if not showtime["seats"][request.seat_label] check comes AFTER the if request.seat_label not in showtime["seats"] check and BEFORE the showtime["seats"][request.seat_label] = False line.
  • Check your terminal for reload errors. If you see a syntax error, double-check the indentation matches the surrounding code.

Why is my booking endpoint still allowing double bookings?

Add booking retrieval endpoints

Your API can create bookings and reject duplicates. Now you need a way to look up existing bookings. This requires a helper function and two new GET endpoints.

  • In main.py, scroll to the # --- Helper Functions --- section. Find the find_showtime function and add this new helper directly below it:
def find_booking(booking_id: str) -> dict | None:
    for booking in bookings:
        if booking["booking_id"] == booking_id:
            return booking
    return None

What does this helper do?

It loops through the bookings list looking for a match on booking_id. Returns the booking dict if found, or None if not. This follows the same pattern as find_showtime.

  • Now scroll to the very end of main.py (after the create_booking function) and add these two endpoints:
@app.get("/bookings", response_model=list[BookingResponse])
def get_bookings():
    return bookings

@app.get("/bookings/{booking_id}", response_model=BookingResponse)
def get_booking(booking_id: str):
    booking = find_booking(booking_id)
    if not booking:
        raise HTTPException(status_code=404, detail="Booking not found")
    return booking

What do these endpoints do?

  • GET /bookings returns the full list of all bookings. Useful for seeing what has been booked so far.
  • GET /bookings/{booking_id} looks up a single booking by its unique ID. Returns 404 if the ID does not exist.
  • Save main.py.
  • Return to http://127.0.0.1:8000/docs. You should see the two new GET /bookings endpoints appear in the docs.
  • First, create a booking by executing POST /bookings with showtime_id 1, seat_label A1, and customer_name Alice. Copy the booking_id from the response.
  • Execute GET /bookings. You should see a list containing the booking you just made.
  • Execute GET /bookings/{booking_id} with the ID you copied. You should see the full booking details returned.

Don't see the new endpoints in the docs?

  • Refresh the docs page in your browser. The auto-reload updates the server, but the browser may cache the old docs page.
  • Check your terminal. If the server shows an error, look for a missing colon or incorrect indentation in the new code.

Why aren't my new GET /bookings endpoints showing up in the docs?

Now test the full error handling suite. You have three types of errors your API should catch.

  • Try POST /bookings with showtime_id set to 99 (does not exist). You should see a 404 response with Showtime not found.
  • Try POST /bookings with showtime_id 1 and seat_label Z9 (does not exist). You should see a 400 response with Seat Z9 does not exist for this showtime.
  • Try POST /bookings with showtime_id 1 and seat_label A1 (already booked). You should see a 409 response with Seat A1 is already booked for this showtime.

All three error scenarios produce clear, descriptive error messages with proper HTTP status codes. Your API now handles every invalid booking attempt gracefully.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and that all three error scenarios (404, 400, 409) return the correct responses.

ⓧ I'd like to double check the full code

Your main.py should contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports: uuid4, FastAPI, HTTPException, status, BaseModel.
  • Data models: Movie, Showtime, BookingRequest, BookingResponse.
  • In-memory data: movies, showtimes, bookings lists.
  • Helper functions: find_showtime and find_booking.
  • Movie endpoints: GET /movies and GET /movies/{movie_id}.
  • Showtime endpoints: GET /showtimes and GET /showtimes/{showtime_id}.
  • Booking endpoints: POST /bookings (with 404, 400, and 409 checks in that order), GET /bookings, GET /bookings/{booking_id}.

Secret mission

Add Booking Cancellation

Your booking API lets customers reserve seats, but what happens when plans change? A real ticketing system needs cancellation. Build a DELETE endpoint that reverses a booking, releases the seat, and rejects attempts to cancel twice.

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. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • FastAPI dev server (running process in your terminal).
  • Python virtual environment (venv folder inside your project).
  • Project folder containing main.py.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep experimenting with your API.

  • Your dev server stays running at http://127.0.0.1:8000.
  • Your project folder and virtual environment remain on your Desktop.
  • You can keep testing endpoints in the interactive docs at http://127.0.0.1:8000/docs.

Pause - I'll come back to this later

Stop the running server to free up your terminal. Your files stay on disk so you can pick up where you left off.

  • Switch to the terminal where your FastAPI server is running.
  • Press Ctrl+C to stop the dev server.
  • Deactivate your virtual environment by running:
deactivate

When you come back, reactivate the virtual environment and restart the server:

# macOS/Linux
source venv/bin/activate

# Windows
venv\Scripts\activate
fastapi dev main.py

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your code, virtual environment, and all installed packages.

  • Switch to the terminal where your FastAPI server is running.
  • Press Ctrl+C to stop the dev server.
  • Deactivate the virtual environment by running:
deactivate
  • Navigate up to your Desktop by running:
cd ~/Desktop
  • Delete the entire project folder by running:

macOS

rm -rf movie-ticket-api

Windows

rmdir /s /q movie-ticket-api

This removes your main.py, virtual environment, and all installed packages in one step.

Nice Work!

Nice Work!

You just built a fully working movie ticket booking API from scratch. It handles real business logic, rejects invalid requests, and documents itself automatically.

Here's what you accomplished:

  • Built a complete REST API with FastAPI that manages movies, showtimes, and bookings with proper CRUD operations.
  • Implemented real business logic including seat availability tracking, double-booking prevention with a 409 Conflict response, and proper HTTP status codes (201, 400, 404, 409).
  • Used Pydantic models for automatic request validation and explored your entire API through auto-generated interactive documentation at /docs.
  • Secret Mission: Added a booking cancellation endpoint that releases seats back to available and handles edge cases like double-cancellation.

Ready to quiz yourself?