Introduction to FastAPI

Session 1: Core Objects

Figure: A sleek, futuristic high-speed train or hyperloop pod arriving at a platform. The platform is clean and modern. With speed lines indicating high performance.

  1. The FastAPI class
  2. The request class
  3. The Request Parameters: from fastapi import Body, Cookie, File, Form, Header, Path, Query
  4. The response class
  5. The Custom Response Classes
  6. The APIRouter class

1. The FastAPI Class

from fastapi import FastAPI

app = FastAPI()

@app.get("/items")
def get_items():
    return [1, 2, 3]

The FastAPI class is the main entry point for your application. It inherits from Starlette and acts as the central hub where you:

  • Register routes using decorators like @app.get(), @app.post(), ..etc.
  • Configure global settings like exception handlers (404, 500, ..etc.).
  • Manage lifecycles: events such as: startup and shutdown

2. The Request Class

Figure: A diagram showing the flow of an HTTP request. A data packet labeled ‘Request’ enters a funnel labeled ‘FastAPI’. The funnel splits the packet into ‘Head’ (Metadata) and ‘Body’ (Payload).

from fastapi import Request

The Request class provides direct access to the underlying HTTP request. This is useful when you need to access:

  • The client’s IP address.
  • Raw Headers or Cookies.
  • The request Body as a raw stream.

3. Request Parameters

from fastapi import Body, Cookie, File, Form, Header, Path, Query

These are the special functions that you can put in path operation function parameters or dependency functions with Annotated to get data from the request.

  • Path & Query: For values in the URL or query string.
  • Body, Form, & File: For data, form fields, or files sent in the request payload.
  • Header & Cookie: For extracting metadata from the request.

4. The Response Class

Figure: A diagram showing the output of the FastAPI funnel. Data comes out and is packaged into a box labeled ‘Response’. A label is being attached to the box that says ‘Status: 200 OK’.

from fastapi import Response

You can declare a parameter in a path operation function or dependency to be of type Response and then you can set data for the response like headers or cookies.

  • Set custom HTTP status codes (e.g., 201 Created).
  • Add custom headers or set cookies.
  • Return different types of content, such as JSON, HTML, or plain text.

5. Custom Response Classes

from fastapi.responses import (
    FileResponse,
    HTMLResponse,
    JSONResponse,
    ORJSONResponse,
    PlainTextResponse,
    RedirectResponse,
    Response,
    StreamingResponse,
    UJSONResponse,
)

Read more about it in the FastAPI docs for Custom Response - HTML, Stream, File, others.

Status Codes

from fastapi import status

It contains a group of named constants (variables) with integer status codes. For example:

  • 200: status.HTTP_200_OK
  • 403: status.HTTP_403_FORBIDDEN
  • etc.

Read more about it in the FastAPI docs about Response Status Code.

6. The APIRouter Class

from fastapi import APIRouter

Group path operations, for example to structure an app in multiple files: routers/items.py and routers/users.py:

.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py

Read more about it in the FastAPI docs for Bigger Applications - Multiple Files.

Running FastAPI: Development

To start your API in development mode, use the command:

fastapi dev main.py
  • Mode: Development
  • Auto-reload: Enabled (Server restarts when code changes).
  • Host: 127.0.0.1 (Localhost).
  • Warning: This mode is resource-intensive and less stable. Do not use in production.

Running FastAPI: Production

To start your API in production mode, use the command:

fastapi run main.py
  • Mode: Production
  • Auto-reload: Disabled (Stable, maximizing performance).
  • Host: 0.0.0.0 (Listen on all available IP addresses).
  • Termination Proxy: Typically runs behind an HTTPS proxy (like Nginx or a Load Balancer).

FastAPI: Standing on Giants

FastAPI is not built from scratch. It creates a powerful synergy by combining two best-in-class libraries:

  1. Starlette: For the web parts.
  2. Pydantic: For the data parts.

Starlette: The Web Engine

Starlette provides the underlying web capabilities:

  • Asynchronous Core: Built on anyio, allowing for high concurrency (dealing with many users at once).
  • WebSockets: Native support for real-time bi-directional communication.

Pydantic: The Data Enforcer

Pydantic provides the data validation and serialization:

Figure: the BaseModel

  • Enforces that input data matches your defined types: "123" \(\rightarrow\) 123
  • Written in Rust (v2), making it one of the fastest validators available.
  • It also draws the map (OpenAPI/Swagger UI) based on your Python code.

Key Takeaways

  • FastAPI Class: The central hub for routing, configuration, and application lifecycle.
  • Request & Response: Use Request for raw access to headers/IPs and Response to manually control status codes and cookies.
  • Modular Routing: APIRouter allows you to split large applications into manageable, logical files.
  • Dev vs. Prod: fastapi dev enables auto-reload for local iteration; fastapi run optimizes for performance and security in production.
  • The “Giants”: FastAPI combines Starlette (for high-concurrency async performance) and Pydantic (for strict, automated data validation and documentation).

Session 2: Syntax and Semantics

Decorators: The @ Symbol

A Decorator is a function that takes another function and extends its behavior without explicitly modifying it.

@app.get("/items")
def get_items():
    return ["a", "b"]

In FastAPI: The @app.get decorator tells FastAPI: “Take the function below (get_items) and register it in the Routing Table for the path /items.”

Type Hints Matter

Type hints are the single definition that drives three separate systems:

  1. Validation & Parsing
  2. Documentation
  3. Auto-completion

Without Types

Manual Parsing:

def create_item(request):
    data = request.json
    
    # 1. Manually check existence
    if "price" not in data:
        return {"error": "Price is required"}, 400
        
    # 2. Manually check type
    if not isinstance(data["price"], (int, float)):
        return {"error": "Price must be a number"}, 400
        
    # 3. Manually parse/convert
    price = float(data["price"])
    
    # ... finally business logic ...

With Type Hints

In FastAPI, you declare the type in the function signature. The framework inspects these signatures at runtime.

from fastapi import FastAPI

app = FastAPI()

# FastAPI reads 'float' and handles validation/conversion for you
@app.post("/items/")
async def create_item(price: float):
    # 'price' is guaranteed to be a float here.
    return {"price_with_tax": price * 1.2}

Complex Data Structures (Pydantic Models)

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    tags: list[str] = [] # Optional list of strings

@app.post("/items/")
async def create_item(item: Item): 
    # FastAPI validates the JSON body against the 'Item' class schema
    return item

Editor Support

The “Invisible” Feature.

Because FastAPI uses standard Python types, your IDE (VS Code, PyCharm) understands the code.

  • Scenario: You type item. (dot) inside your function.
  • Result: The editor immediately suggests .name, .price, and .tags because it knows item is of type Item.

Constraints: Field

Pydantic uses Field or specialized types to define further constraints.

from pydantic import Field

class User(BaseModel):
    # Set: {Strings with length between 3 and 10}
    username: str = Field(min_length=3, max_length=10)

The Annotated

Annotated was introduced (in PEP 593) to allow “tool-specific metadata” to exist alongside types:

from typing import Annotated
from pydantic import BaseModel, Field

class User(BaseModel):
    # The 'int' is the type, the 'Field' is the metadata tag
    age: Annotated[int, Field(gt=18, lt=100)]
  • For editors and other tools, the type of age is still the first thing: str.
  • For Pydantic, it knows to enforce additional constraints specified by Field.

Using Type Hints

class Fruit(BaseModel):
    # Exclusive OR: either 'red' or 'green'
    color: Literal['red', 'green']

    # Nested Structure
    bazam: dict[str, list[tuple[int, bool, float]]]  

Custom Parsing Functions

If you wish to customize:

  • On failure: raise ValueError
  • On success: return
class User(BaseModel):
    # Set: {Integers that are even} (Custom Subset)
    even_id: int

    @field_validator('even_id')
    def must_be_even(cls, v):
        if v % 2 != 0: raise ValueError("Must be even")
        return v

Key Takeaways

  • Blueprints: Use Python classes for automatic validation and parsing.
  • DX: Type hints enable auto-completion and catch errors early.
  • Constraints: Use Field and Annotated for precise data rules.
  • Validation: Use @field_validator for custom business logic.