Cover image for FastAPI Fundamentals: Routing, Pydantic Models, and Dependency Injection

At a glance

Reading time

~200 words/min

Published

2 hours ago

Jun 10, 2026

Views

3

All-time total

FastAPI Fundamentals: Routing, Pydantic Models, and Dependency Injection

Part 4 turns your Python skills into a real API. FastAPI combines the typing and Pydantic foundations from earlier parts into a framework that validates requests, generates docs, and runs async out of the box. This part covers the three things you use in every endpoint: routing, request and response models, and dependency injection.

What you will build

  • Typed routes with path and query parameters
  • Request and response models that validate automatically
  • Dependency injection for shared resources and auth
  • Automatic interactive docs at /docs
i

Info

Install

Add FastAPI and an ASGI server with uv add "fastapi[standard]". The standard extra includes uvicorn and the CLI used below.

1. A typed route

A FastAPI route is a plain async function with type hints. FastAPI reads the hints to parse and validate inputs, and to document the endpoint. Path parameters come from the URL, query parameters from the query string.

from fastapi import FastAPI

app = FastAPI(title="Projects API")

@app.get("/projects/{project_id}")
async def get_project(project_id: int, include_archived: bool = False) -> dict:
    return {"id": project_id, "include_archived": include_archived}

Run it with uv run fastapi dev src/llm_app/main.py and open /docs. The id is validated as an integer and the flag defaults to false, with no manual parsing.

2. Request and response models

Use Pydantic models for bodies. FastAPI validates the incoming JSON against the request model and shapes the output to the response model, so you never leak internal fields by accident.

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class ProjectIn(BaseModel):
    name: str = Field(min_length=3, max_length=120)
    status: str = "draft"

class ProjectOut(BaseModel):
    id: int
    name: str
    status: str

@app.post("/projects", response_model=ProjectOut, status_code=201)
async def create_project(payload: ProjectIn) -> ProjectOut:
    return ProjectOut(id=1, name=payload.name, status=payload.status)

3. Dependency injection

A dependency is a function FastAPI calls for you and injects the result. Use it for shared resources like a database session or an HTTP client, and for cross cutting concerns like authentication. The same dependency can guard many routes.

from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()

async def require_api_key(x_api_key: str = Header(...)) -> str:
    if x_api_key != "secret-key":
        raise HTTPException(status_code=401, detail="invalid api key")
    return x_api_key

@app.get("/secure")
async def secure(api_key: str = Depends(require_api_key)) -> dict:
    return {"ok": True}

The route never runs unless the dependency passes, and the dependency is testable on its own. This is the same idea you practiced in Part 2 and Part 3, now wired into the framework.

Checkpoint

What does the response_model parameter do?

4. Validating path and query parameters

Type hints handle parsing, but you often want rules too: a page number that must be positive, a search term with a maximum length, a limit capped to protect the database. FastAPI exposes Path and Query for exactly this, and a failed rule produces the same clean validation error as a bad body. You declare the constraint once and never write a manual guard.

from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/projects/{project_id}/items")
async def list_items(
    project_id: int = Path(ge=1),
    q: str | None = Query(default=None, max_length=80),
    limit: int = Query(default=20, ge=1, le=100),
) -> dict:
    return {"project_id": project_id, "q": q, "limit": limit}

5. Errors and status codes

Returning a 200 with an error message hidden in the body is a habit worth breaking. Raise HTTPException with the right status code and let FastAPI shape the response. Clients, logs, and monitoring all rely on the status line, so a missing record should be a 404 and a forbidden action a 403, not a 200 that quietly contains the word error.

from fastapi import FastAPI, HTTPException

app = FastAPI()
DB = {1: "Search API"}

@app.get("/projects/{project_id}")
async def get_project(project_id: int) -> dict:
    name = DB.get(project_id)
    if name is None:
        raise HTTPException(status_code=404, detail="project not found")
    return {"id": project_id, "name": name}

For validation errors you do nothing at all: when a request body fails its Pydantic model, FastAPI returns a 422 with a detail list naming each bad field. That is the behavior you will assert on in the testing part, and it is free because of the modeling work from Part 2.

6. Routers keep the app from sprawling

A single file with every route becomes unmanageable fast. APIRouter lets you group related routes into their own module with a shared prefix and tags, then include them in the app. This is the structure Part 5 builds on, and it keeps each feature, projects, chat, search, in a file you can read top to bottom.

# routers/projects.py
from fastapi import APIRouter

router = APIRouter(prefix="/projects", tags=["projects"])

@router.get("")
async def list_projects() -> list[dict]:
    return [{"id": 1, "name": "Search API"}]

# main.py
from fastapi import FastAPI
from routers import projects

app = FastAPI()
app.include_router(projects.router)

7. The docs are not an afterthought

Because every route is typed and every model is declared, FastAPI generates an OpenAPI schema and serves interactive docs at /docs and a clean reference at /redoc. This is genuinely useful, not a checkbox. Frontend developers read it, you click Try it out to exercise an endpoint without writing a client, and the schema can generate typed API clients. Add a summary and description to a route and it shows up in the docs automatically.

@app.post(
    "/projects",
    summary="Create a project",
    description="Creates a project and returns it with a generated id.",
    response_model=ProjectOut,
    status_code=201,
)
async def create_project(payload: ProjectIn) -> ProjectOut:
    return ProjectOut(id=1, name=payload.name, status=payload.status)
💡

Pro tip

Let the framework do the work. If you find yourself manually parsing query strings, hand-writing validation, or maintaining API docs in a separate document, there is almost always a FastAPI feature that does it from your type hints instead.

8. Putting the pieces together

Routing, models, dependencies, and errors are not separate tricks, they combine into one readable endpoint. The handler below validates the body with a model, pulls a shared resource through a dependency, returns a 404 when appropriate, and shapes its output with a response model. Read it and notice how little glue code there is: the framework does the parsing, validation, and serialization, leaving you with the actual logic.

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field

router = APIRouter(prefix="/projects", tags=["projects"])
DB: dict[int, dict] = {}

class ProjectIn(BaseModel):
    name: str = Field(min_length=3, max_length=120)

class ProjectOut(BaseModel):
    id: int
    name: str

async def next_id() -> int:           # a stand-in shared dependency
    return len(DB) + 1

@router.post("", response_model=ProjectOut, status_code=201)
async def create(payload: ProjectIn, new_id: int = Depends(next_id)) -> ProjectOut:
    DB[new_id] = {"id": new_id, "name": payload.name}
    return ProjectOut(**DB[new_id])

@router.get("/{project_id}", response_model=ProjectOut)
async def read(project_id: int) -> ProjectOut:
    if project_id not in DB:
        raise HTTPException(status_code=404, detail="project not found")
    return ProjectOut(**DB[project_id])

Everything here is testable in isolation, which is the quiet benefit of building it this way. The model can be validated on its own, the dependency is a plain function you can call directly, and in Part 7 you will drive the whole endpoint with the test client and override next_id without changing a line of the handler.

9. Why this scales

The reason this structure holds up is that responsibilities stay separated. Validation lives in models, shared resources and policy live in dependencies, and routes stay thin. When a requirement changes, you usually touch one of those layers and not the others. Add a field and you edit a model. Add auth and you add a dependency. Add an endpoint and you add a route to a router. That separation is what keeps an API maintainable as it grows from a demo into a product.

i

Info

The docs prove the contract

Because the example above is fully typed, /docs shows the request and response shapes, the 201 and 404 responses, and a working Try it out form, with no extra effort. If a field is missing from the docs, it is missing a type hint.

The bottom line

FastAPI rewards the groundwork from the earlier parts. Type hints drive routing, Pydantic drives validation, and dependency injection keeps shared resources and auth clean. With these three pieces you can build most of an API, and because each layer stays separate, the app keeps its shape as it grows: models hold validation, dependencies hold shared resources and policy, and routes stay thin. You also get accurate interactive docs for free, which means the contract your frontend reads can never silently drift from the code. Next we harden all of this for production with typed settings, real authentication, middleware you can see through, and a project structure that does not collapse under its own weight.

? Frequently asked questions

Is FastAPI fast enough for production? +

Yes. On an ASGI server like uvicorn it is among the fastest Python frameworks, and most latency in an LLM app comes from the model, not the framework.

Do I need async for every route? +

No, but use async for routes that do network or database I/O so the server can handle other requests while waiting.

Up next: Part 5, FastAPI in production.

Newsletter

Want more posts like this?

Get practical software notes and tutorials delivered when something new is published.

No spam. Unsubscribe anytime.

How did this land?

Comments

0
Log in or sign up to join the discussion and react to this post.

No comments yet. Be the first to share your thoughts.

Related posts

FastAPI in Production: Settings, Auth, Middleware, and Project Structure

Harden a FastAPI app for production: typed settings with pydantic-settings, bearer auth, logging and CORS middleware, and a scalable project structure.

2 hours ago

Streaming and Background Work in FastAPI: SSE and BackgroundTasks

Stream responses from FastAPI with server sent events, run side effects with BackgroundTasks, and know when to move to a real task queue.

2 hours ago

Testing FastAPI the Right Way: pytest, the Test Client, and Validation

Test FastAPI with pytest and the test client: assert on validation, override dependencies to isolate from real services, and cover async and streaming code.

2 hours ago