Skip to main content

Hello FastAPI

If you're new to programming

FastAPI turns Python functions into web endpoints that respond to HTTP requests. This lesson gets a server running in minutes. You will see your API in a browser.

If you've coded before

FastAPI = Starlette + Pydantic + OpenAPI auto-docs. Async-native. uvicorn as ASGI server. --reload for development. This lesson covers installation, first routes, and the /docs interactive UI.

In Lesson 1, you learned what a web API is: HTTP methods, URLs, status codes, request/response with JSON. Now you build one.

James has a question: "I know how to write Python functions. I know how to handle async. How does a browser call a Python function?" The answer is FastAPI. It maps HTTP routes to Python functions. When a browser sends GET /health, FastAPI calls your health() function and sends the return value back as JSON.


Installing FastAPI

FastAPI is not part of the Python standard library. You need two packages:

  • fastapi: The framework that maps routes to functions
  • uvicorn: The server that listens for HTTP requests and passes them to FastAPI

Add both to your SmartNotes project:

uv add fastapi uvicorn

Output:

Resolved 12 packages in 1.2s
Installed 12 packages in 0.8s
+ anyio==4.9.0
+ fastapi==0.115.12
+ uvicorn==0.34.2
...

Check your pyproject.toml. The [project.dependencies] section now includes fastapi and uvicorn.


Your First Route

Create a new file at smartnotes/api.py:

from fastapi import FastAPI

app: FastAPI = FastAPI()


@app.get("/")
async def root() -> dict[str, str]:
return {"status": "SmartNotes API running"}

Four lines. Here is what each one does:

LineWhat it does
from fastapi import FastAPIImports the framework
app: FastAPI = FastAPI()Creates an application instance
@app.get("/")Registers the function for GET / requests
async def root() -> dict[str, str]The function FastAPI calls when the route matches

The @app.get("/") decorator works like argparse subcommands from Ch 65. Instead of mapping a CLI command to a function, it maps an HTTP route to a function. The return value becomes the JSON response.

Start the development server:

uv run uvicorn smartnotes.api:app --reload

Output:

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [12345]
INFO: Started server process [12346]
INFO: Waiting for application startup.
INFO: Application startup complete.

The --reload flag watches your files and restarts the server when you save changes. Open your browser to http://localhost:8000/.

Browser output:

{"status":"SmartNotes API running"}

Your Python function is running behind an HTTP server. The browser sent GET /, uvicorn received it, FastAPI called root(), and the return dictionary was serialized to JSON.


The /docs Page

Navigate to http://localhost:8000/docs in your browser.

FastAPI auto-generates an interactive documentation page from your code. Every route you define appears here with its method, URL, parameters, and response format. You can test routes directly from this page without writing any client code.

Try it:

  1. Click the GET / row to expand it
  2. Click Try it out
  3. Click Execute
  4. See the response body, status code, and response headers

This page updates automatically as you add routes. The --reload flag means you do not even need to restart the server. Save a new route in api.py, refresh /docs, and it appears.

The documentation is generated from the OpenAPI standard (formerly Swagger). FastAPI reads your function signatures, type annotations, and Pydantic models to build the specification. The better your type annotations, the better the documentation.


Adding a Health Endpoint

A health endpoint tells monitoring tools whether your API is running. Add it below your root route in smartnotes/api.py:

from datetime import datetime, timezone

from fastapi import FastAPI

app: FastAPI = FastAPI()

_start_time: datetime = datetime.now(timezone.utc)


@app.get("/")
async def root() -> dict[str, str]:
return {"status": "SmartNotes API running"}


@app.get("/health")
async def health() -> dict[str, str | int]:
uptime_seconds: int = int(
(datetime.now(timezone.utc) - _start_time).total_seconds()
)
return {
"status": "healthy",
"version": "0.1.0",
"uptime_seconds": uptime_seconds,
}

Save the file. The server restarts automatically (because of --reload). Open http://localhost:8000/health:

Output:

{"status":"healthy","version":"0.1.0","uptime_seconds":42}

Now check /docs. Both routes appear: GET / and GET /health. Click Try it out on the health endpoint and observe the uptime increasing each time you execute it.

Notice the type annotations. The return type dict[str, str | int] tells FastAPI (and the docs page) that the response contains string keys with string or integer values. FastAPI uses these annotations to generate accurate documentation. This is the same type annotation system from Ch 55 (Pydantic) and Ch 47 (primitive types). Everything connects.


PRIMM-AI+ Practice: Add and Test a Route

Predict [AI-FREE]

Press Shift+Tab to enter Plan Mode.

You are about to add a third route:

@app.get("/notes/count")
async def notes_count() -> dict[str, int]:
return {"count": 0}

Before adding it, predict:

  1. What URL will this route respond to?
  2. What HTTP method will it accept?
  3. What will the JSON response look like?
  4. Will it appear in /docs automatically?

Rate your confidence from 1 to 5.

Run

Press Shift+Tab to exit Plan Mode.

Add the route to smartnotes/api.py. Save the file. Open http://localhost:8000/notes/count in your browser. Then check /docs.

Compare each answer to your prediction.

Investigate

Run /investigate @smartnotes/api.py in Claude Code and ask: "What happens if I define two routes with the same URL but different methods, like @app.get('/notes') and @app.post('/notes')? Can FastAPI handle that?"

This leads to the GET/POST combination you will build in Lesson 3.

Modify

Change the notes_count route to accept a query parameter:

@app.get("/notes/count")
async def notes_count(tag: str | None = None) -> dict[str, int | str | None]:
return {"count": 0, "filter": tag}

Visit http://localhost:8000/notes/count?tag=python and http://localhost:8000/notes/count (no tag). Predict the difference before checking.

Make [Mastery Gate]

Add a GET /version route that returns the SmartNotes version, Python version, and FastAPI version. Use sys.version for Python and fastapi.__version__ for FastAPI.

Use the /tdg cycle:

  1. Write the function stub with full type annotations
  2. Write a test (you will learn TestClient in Lesson 4, so for now, test by running the server and using curl or the /docs page)
  3. Implement the route
  4. Verify by checking http://localhost:8000/version

Try With AI

Opening Claude Code

If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.

Prompt 1: Understand the Server

I have this FastAPI code running:

from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def root() -> dict[str, str]:
return {"status": "SmartNotes API running"}

I start it with: uv run uvicorn smartnotes.api:app --reload

Explain the complete chain: what happens from the moment
I type localhost:8000/ in my browser to the moment I see
the JSON response? Walk through each step: browser,
HTTP request, uvicorn, FastAPI routing, function call,
JSON serialization, HTTP response, browser rendering.

What you're learning: Understanding the full request lifecycle helps you debug problems later. When a route does not respond, you need to know where in the chain the failure occurred: DNS, TCP, uvicorn, route matching, function execution, or serialization. The AI traces each step so you can pinpoint failures.

Prompt 2: Compare to CLI

In Chapter 65, I mapped CLI commands to Python functions
using argparse. Now I am mapping HTTP routes to Python
functions using FastAPI decorators.

Compare these two mapping systems:
- argparse: parser.add_argument() → function call
- FastAPI: @app.get("/path") → function call

What is similar? What is different? Which approach
handles input validation better, and why?

What you're learning: CLI argument parsing and HTTP route handling solve the same problem: mapping external input to Python function calls. FastAPI's type annotations provide validation that argparse requires extra code for. Recognizing this parallel deepens your understanding of both tools.

Prompt 3: Explore /docs Features

I opened http://localhost:8000/docs and I see my routes
listed. I know I can click "Try it out" to test them.

What else can the /docs page do? Can it show me:
1. The expected request body format for POST routes?
2. The response schema?
3. Authentication requirements?
4. Example values?

Also, what is the difference between /docs and /redoc?

What you're learning: The /docs page is more powerful than a simple route tester. It shows schemas, validates inputs, and documents your API for other developers. Understanding its features saves time: you do not need to write separate API documentation. FastAPI generates it from your code.


James refreshes the /docs page. Three routes, all documented, all testable from the browser. "I wrote three functions and got a fully documented API."

Emma smiles. "I still find /docs impressive every time. FastAPI's auto-documentation is one of the best developer experience features in any Python library. I once spent two weeks writing API documentation by hand for a Flask project. Two weeks. FastAPI generates it for free."

"But these routes return hardcoded data," James says. "The count is always 0. The health endpoint does not check if the storage file exists."

"Right. Your routes need to accept real data and return real data. That means request models and response models. Pydantic models, specifically, the same BaseModel you used in Ch 55."

"Pydantic again?"

"FastAPI was designed around it. Every request body, every response body, every query parameter can be a Pydantic model. Validation happens automatically. If someone sends bad data, FastAPI returns a 422 with the exact validation error, and you do not write a single line of validation code."