AlgoMaster Logo

FastAPI Basics

Medium Priority21 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

FastAPI is a modern, async-first Python web framework built on top of Starlette (the ASGI toolkit) and Pydantic (the validation library). It uses Python type hints to drive request validation, response serialization, and automatic OpenAPI documentation. This lesson covers what FastAPI is, how to write a hello-world endpoint, how path operations work, how Pydantic models shape requests and responses, dependency injection, async endpoints, and how FastAPI compares with Flask and Django.

What FastAPI Is

FastAPI was released in 2018 by Sebastián Ramírez. Two things made it stand out from the existing crop of Python web frameworks. The first was async-first design: every endpoint can be a coroutine, and the framework runs on an ASGI server (uvicorn or hypercorn) by default. The second was deep integration with Python's type hints: a function that declares name: str as a parameter gets string validation, OpenAPI schema generation, and editor autocomplete from that single annotation.

The result is a framework that takes less code per endpoint than Flask, gives more correctness checks than either Flask or Django, and pays no performance penalty for it. FastAPI is consistently among the fastest Python web frameworks in independent benchmarks, largely because Starlette is fast and Pydantic v2 is implemented in Rust.

The diagram traces a typical FastAPI request. Pydantic does the heavy lifting on both sides: it parses and validates the incoming body, and serializes the return value back to JSON. The endpoint function in the middle is plain Python, free to be sync or async, and only sees fully validated data.

Hello World

The smallest useful FastAPI app fits in five lines:

Save that as main.py and run it with:

Open http://127.0.0.1:8000/ in a browser and the response is {"message": "Welcome to the store"}. The function returned a dict, FastAPI serialized it to JSON, set the Content-Type header, and sent the response. No HTML, no templates, no router configuration beyond the decorator.

Two URLs are available without any extra code:

  • http://127.0.0.1:8000/docs shows interactive Swagger UI documentation
  • http://127.0.0.1:8000/redoc shows the same API documented with ReDoc

Both are generated from the type hints and decorators in the application code. There's no separate spec file to maintain. As endpoints, parameters, and models are added, the docs update automatically. This is the feature that has made FastAPI the default choice for many teams shipping JSON APIs.

--reload is convenient during development because uvicorn restarts on every file change. It's strictly a development tool and should never be used in production: the reloader uses extra memory, checks the filesystem on every interval, and isn't designed for the load a real deployment sees.

Path Operations

A "path operation" in FastAPI vocabulary is the combination of an HTTP method and a URL path. The decorators on app mirror the HTTP verbs: @app.get, @app.post, @app.put, @app.patch, @app.delete, plus @app.head and @app.options for completeness.

Three endpoints: list products, get one by ID, create a new one. The path /products/{product_id} declares a path parameter, and the product_id: int annotation tells FastAPI to validate that the captured value is a valid integer. A request to /products/abc returns a 422 with a structured error explaining what went wrong, without the endpoint ever running.

The type hint isn't a suggestion. FastAPI uses it as the validation rule, the OpenAPI schema, and the editor autocomplete source, all at once. Three pieces of information from one annotation.

Pydantic Models for Requests and Responses

For anything more complex than a single value, FastAPI uses Pydantic models. A Pydantic model is a class that inherits from BaseModel, declares fields with type hints, and gets validation, serialization, JSON schema generation, and editor support from those declarations.

ProductCreate is the input shape: a name (string, required), a price (float, required), and a stock count (integer, defaulting to 0). When a client POSTs JSON to /products, FastAPI parses the body, validates it against ProductCreate, and passes the resulting Python object to the endpoint as product. If validation fails, the endpoint never runs and the client gets a structured 422 error.

ProductOut is the output shape. The response_model=ProductOut argument on the decorator tells FastAPI to serialize the return value through that model, which trims extra fields and converts types as needed. The same model also feeds the OpenAPI schema, so the docs show exactly what the endpoint returns.

A POST with this body:

returns:

Pydantic models compose. A model can have fields of other model types, list-of-model types, dict-valued fields, optional fields with Optional[X] or X | None, and discriminated unions for polymorphism. Validators (@field_validator, @model_validator) add custom checks beyond the type hint.

Field adds metadata that becomes part of validation and the JSON schema. min_length, max_length, gt (greater than), ge (greater or equal), lt, le, pattern, default, and many more are first-class. The @field_validator decorator adds custom logic that runs after type coercion. Together, this gives a precise description of valid input, in one class, that the framework reads to validate requests, generate docs, and inform editor autocomplete. Pydantic validation runs on every request that hits a model-typed endpoint. For most apps this is fast (Pydantic v2 is implemented in Rust and is roughly 5-50x faster than v1). For extreme hot paths with deeply nested models, profile and use model_construct to skip validation when the input source is trusted.

Query, Path, and Body Parameters

FastAPI infers where each parameter comes from based on its type and position. The rules are simple:

  • A parameter that matches a name in the URL pattern (like product_id in /products/{product_id}) is a path parameter.
  • A parameter with a primitive type (int, str, float, bool) that isn't in the URL is a query parameter.
  • A parameter with a Pydantic model type is a request body.

category and limit are query parameters because neither is in the URL and both are primitives. A request to /products?category=mugs&limit=50 arrives with category="mugs" and limit=50. The default values make both optional. category: str | None = None makes category truly optional (it will be None if not supplied); limit: int = 20 defaults to 20 if not supplied.

For explicit control, FastAPI exposes Query, Path, and Body markers from the fastapi package:

Path(gt=0) says the path parameter must be greater than zero. Query(default=False) is the default for the query parameter. The same gt, lt, min_length, max_length, pattern keywords from Pydantic work here. The endpoint stays a normal function; the markers attach metadata.

For complex query strings with many parameters, group them into a Pydantic model with a Query() annotation (FastAPI 0.110+), but the simple shape (one parameter per query value) covers most cases.

Dependency Injection

FastAPI has a small dependency injection system based on the Depends marker. A dependency is a callable (usually a function) that FastAPI runs before the endpoint, passing its return value as a parameter. Dependencies can themselves declare dependencies, forming a DAG that FastAPI resolves automatically.

A common use is database sessions. Open a session per request, hand it to the endpoint, close it after the response is sent:

get_db is a generator function: the part before yield runs before the endpoint, the part after yield runs after the response is sent. This is the standard pattern for resources that need setup and teardown.

Another common use is authentication. A dependency that reads the Authorization header, validates the token, and returns the user:

If the token is invalid, the dependency raises HTTPException(status_code=401) and the endpoint never runs. If it's valid, the user object arrives at the endpoint as a parameter.

Dependencies can depend on other dependencies. A current_admin dependency that builds on current_user:

FastAPI resolves the DAG, runs each dependency once per request, and caches the result so multiple endpoints sharing the same dependency don't pay for it twice.

Async Endpoints

Endpoints can be sync (def) or async (async def). FastAPI runs both correctly: sync endpoints run in a threadpool so they don't block the event loop, async endpoints run directly on the loop.

async def allows the endpoint to use await on any awaitable. While the HTTP request to the upstream service is in flight, the event loop is free to handle other requests. For an API that mostly waits on other services, this is a meaningful throughput win: hundreds of concurrent requests on a single process without the overhead of one thread per request.

The choice between sync and async comes down to what the endpoint does:

Endpoint does mostly...Use
In-memory work, simple calculationsdef (sync is fine)
Sync database calls (SQLAlchemy sync, the standard requests library)def (FastAPI runs it in a threadpool)
Async database calls (asyncpg, databases, SQLAlchemy async, aiomysql)async def
Async HTTP calls (httpx.AsyncClient, aiohttp)async def
External services that block for a long timeasync def with an async client

The pitfall is calling synchronous blocking code inside async def without going through a threadpool. requests.get(...) inside async def blocks the event loop for the duration of the HTTP call, which freezes every other concurrent request. The fix is either to switch to httpx.AsyncClient (and await it) or to run the blocking call in a threadpool with await asyncio.to_thread(...). The same rule applies to file I/O, sync ORM calls, and any third-party library that wasn't built for async.

If the endpoint must call sync code (a legacy library that doesn't have an async version), make the function sync. FastAPI will run it in a threadpool automatically, which is fine for moderate load.

A common FastAPI performance bug is "made the endpoint async, but called a sync blocking library inside it." The endpoint looks async and runs on the event loop, but blocks the whole loop on every call. Symptoms include high tail latency under load and one slow endpoint making the whole server slow. Use async-native libraries inside async endpoints, or stay sync.

Response Models and Status Codes

The response_model parameter on the decorator controls how the return value is serialized. It can trim fields the endpoint shouldn't expose (passwords, internal IDs), convert types, and feed the OpenAPI schema.

The function returns a dict that has a password_hash field. The response_model=UserPublic argument filters the output through that model, so the response includes only id, username, and email. The password_hash never leaves the server. The status_code=201 argument sets the response status to "Created" instead of the default 200.

For different status codes per response type, the responses argument on the decorator documents them in OpenAPI:

Raising HTTPException(status_code=404, detail="Product not found") from inside the endpoint produces a structured 404 response with the detail in the body. Custom error formats can be set up through exception handlers, but the default is usually sufficient.

WebSockets in Brief

FastAPI supports WebSockets through Starlette's WebSocket protocol. The shape is similar to HTTP endpoints but uses @app.websocket and an async function that takes a WebSocket parameter:

The endpoint accepts the connection, loops on incoming messages, and echoes them back. Production WebSocket endpoints typically broadcast to multiple connected clients, integrate with a message broker for cross-process state, and handle disconnect cleanly. The basic shape is what matters here: an async function decorated with @app.websocket, taking a WebSocket object that exposes accept, receive_text, receive_bytes, receive_json, send_*, and close methods.

WebSockets are a small but important part of FastAPI's value proposition. Django's WebSocket story requires Channels (an extra package); FastAPI's is built in because Starlette has supported them from day one.

Running FastAPI

Development uses uvicorn with auto-reload, as shown above. Production uses uvicorn (or hypercorn) behind a process manager that runs multiple workers.

That command runs four uvicorn worker processes, each with its own event loop, sharing the port through SO_REUSEPORT. Behind that, a reverse proxy like nginx handles TLS, static files, and HTTP/2.

For richer process management, gunicorn with uvicorn workers is a common pattern:

Gunicorn handles the worker lifecycle (graceful reload, max-requests recycling, worker timeout), uvicorn runs the async event loop inside each worker. This is the recommended production pattern for FastAPI.

The number of workers is usually 2 * cores + 1 for CPU-bound work or higher for I/O-bound work. Async endpoints inside a single worker handle large concurrency on their own, so even one worker can sustain thousands of concurrent connections, but multiple workers provide fault isolation (a worker crash takes out one in N requests, not all of them).

FastAPI vs Flask

Flask and FastAPI are both Python web frameworks for building HTTP APIs. The differences are substantial enough that the choice usually leans one way or the other.

ConcernFlaskFastAPI
Async supportOptional, added later, not idiomaticNative, built on ASGI
Request validationManual (Marshmallow, Pydantic, hand-rolled)Built-in via Pydantic
OpenAPI docsExtension (Flask-RESTX, flask-smorest)Built-in, auto-generated
Type hint integrationNone by defaultDrives the whole framework
RoutingDecorator-basedDecorator-based (same shape)
ORMBring your own (Flask-SQLAlchemy is common)Bring your own (SQLAlchemy, Tortoise, SQLModel)
TemplatingBuilt-in (Jinja2)Optional (Jinja2 via Starlette)
WebSocketsExtension (Flask-SocketIO)Built-in
PerformanceSolid but syncAmong the fastest Python frameworks
Maturity2010, huge ecosystem2018, fast-growing ecosystem
Best matchSmall apps, server-rendered HTML, legacy projectsJSON APIs, async services, type-hint-driven codebases

A Flask endpoint for the same product-create operation looks like this:

The Flask version does the validation by hand. The FastAPI version with a Pydantic model is shorter and produces the OpenAPI docs as well:

For new projects building JSON APIs, FastAPI is usually the better starting point. For projects that need server-rendered HTML, are already using Flask, or have lots of synchronous code, Flask is still a fine choice. For projects needing batteries-included admin, ORM, and auth, Django fits better.

Quiz

FastAPI Basics Quiz

10 quizzes