Mid-level 3 min · March 06, 2026

FastAPI Basics: Build Your First Python API in Minutes

Master FastAPI fundamentals: routing, Pydantic validation, and dependency injection.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • FastAPI is a Python web framework that builds on Python type hints for automatic validation, serialization, and docs.
  • Path parameters identify resources (/users/42), query filters filter collections (/users?role=admin), request bodies carry complex JSON.
  • Use async def for I/O-bound endpoints; plain def for CPU-bound work (FastAPI runs them in a thread pool).
  • Dependency injection via Depends() lets you share authentication, DB sessions, and config cleanly.
  • Performance: ASGI-native + async allows handling 1000s of concurrent connections without thread overhead.
  • Biggest mistake: calling synchronous requests library inside async endpoint — blocks the event loop and kills concurrency.
Plain-English First

Imagine you run a restaurant. Customers (browsers, apps, devices) shout orders through a window. FastAPI is the super-efficient waiter who takes the order, checks it makes sense (no one ordered 'purple soup'), passes it to the kitchen (your Python logic), and hands back the meal — all at lightning speed. It even writes the menu board automatically so customers always know what they can order. That menu board is your API documentation, generated for free the moment you write your code.

(already enriched above)

Why FastAPI Exists — and Why It Beats Flask for New Projects

Flask was designed in 2010. Python type hints didn't exist until 2015. Async/await didn't land until Python 3.5. Flask was never built with these features in mind, so adding them feels like bolting wings onto a car.

FastAPI was designed in 2018 specifically around type hints and the ASGI (Asynchronous Server Gateway Interface) standard. That's not just a version number difference — it's a completely different philosophy. When you write a type hint in FastAPI, the framework actually reads it at startup and uses it to validate incoming data, serialize outgoing data, and generate documentation. You write the types once and get three things for free.

The performance difference is real too. Because FastAPI is ASGI-native and supports Python's async/await, it can handle thousands of simultaneous I/O-bound requests without spawning new threads. Benchmarks consistently put it alongside Node.js and Go for throughput — which is extraordinary for Python.

Use FastAPI when you're building a new API from scratch, especially if your endpoints touch databases, external services, or any I/O. Use Flask if you're maintaining an existing Flask codebase or need a tiny one-file script server where the overhead of learning something new isn't worth it.

io/thecodeforge/basics/hello_api.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Install first: pip install fastapi uvicorn
# Run with:    uvicorn hello_api:app --reload

from fastapi import FastAPI

# FastAPI() creates the application instance.
# Think of it as 'opening the restaurant for business.'
app = FastAPI(
    title="TheCodeForge Demo API",
    description="A minimal FastAPI example that proves how little code you need.",
    version="1.0.0"
)

# The @app.get decorator registers this function as the handler
# for HTTP GET requests to the root path "/".
@app.get("/")
def read_root() -> dict:
    # FastAPI converts Python dicts to JSON automatically.
    return {"message": "Welcome to TheCodeForge API", "status": "running"}

# A second endpoint at /health — useful for monitoring tools
@app.get("/health")
def health_check() -> dict:
    return {"healthy": True}
Output
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
# Visiting http://127.0.0.1:8000/ returns:
{"message": "Welcome to TheCodeForge API", "status": "running"}
# FREE BONUS: Visit http://127.0.0.1:8000/docs for interactive Swagger UI
Pro Tip: --reload is Your Best Friend
Always start your dev server with uvicorn hello_api:app --reload. The --reload flag watches your files for changes and restarts the server automatically. Without it you'll spend half your day Ctrl+C-ing and re-running. Never use --reload in production — it adds overhead and is a security risk.
Production Insight
FastAPI reads type hints at import time — any import error or type issue crashes the entire server on startup.
Always test with uvicorn app:app without --reload before deploying.
Rule: validate your models in isolation before wiring them into routes.
Key Takeaway
Type hints are not just for your IDE — FastAPI turns them into runtime validation and doc generation.
One type annotation gives you validation, serialization, and docs for free.
Never underestimate the productivity gain of writing types once.

Path Parameters, Query Parameters, and Pydantic Request Bodies

Every API needs to accept input. FastAPI gives you three clean ways to do it, each suited to a different purpose — and confusing them is one of the most common beginner mistakes.

Path parameters are part of the URL itself: /users/42 where 42 is the user ID. They identify a specific resource. Query parameters come after the ? in the URL: /products?category=books&limit=10. They filter, sort, or paginate a collection. Request bodies are sent in the HTTP body (usually as JSON) and carry complex structured data.

FastAPI's magic is that you declare all three using nothing but Python function signatures. A path parameter is a function argument that matches a {placeholder} in the route. A query parameter is a function argument that doesn't match any placeholder. A request body is a function argument typed as a Pydantic model.

io/thecodeforge/basics/book_api.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI(title="Bookstore API")

# --- Pydantic Model ---
class Book(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    author: str = Field(..., min_length=2)
    year: int = Field(..., ge=1000, le=2100)
    genre: Optional[str] = None

books_db: dict[int, Book] = {
    1: Book(title="Clean Code", author="Robert C. Martin", year=2008, genre="Software Engineering"),
}
next_id = 2

# --- PATH PARAMETER ---
@app.get("/books/{book_id}")
def get_book(book_id: int) -> Book:
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail=f"Book {book_id} not found")
    return books_db[book_id]

# --- QUERY PARAMETERS ---
@app.get("/books")
def list_books(genre: Optional[str] = None, limit: int = 10) -> list[Book]:
    all_books = list(books_db.values())
    if genre:
        all_books = [b for b in all_books if b.genre and b.genre.lower() == genre.lower()]
    return all_books[:limit]

# --- REQUEST BODY ---
@app.post("/books", status_code=201)
def create_book(new_book: Book) -> dict:
    global next_id
    books_db[next_id] = new_book
    created_id = next_id
    next_id += 1
    return {"message": "Book created successfully", "id": created_id}
Output
# POST /books with invalid year returns 422 Unprocessable Entity automatically.
Watch Out: Path Parameter Order Matters
If you define a route /books/featured and another /books/{book_id}, always register /books/featured FIRST in your file. FastAPI matches routes top-to-bottom, so if {book_id} comes first, the word 'featured' gets treated as a book ID and your specific route never triggers.
Production Insight
Pydantic validation errors return 422 with a detailed JSON body — but clients often ignore the body.
Log the full validation error on the server side for debugging.
Rule: always include field-level error messages in your API responses for better client integration.
Key Takeaway
Path params = identify resource. Query params = filter/sort. Request body = complex data.
FastAPI infers parameter type from the function signature — no extra decorators needed.
The 422 validation error is your friend — parse it, don't ignore it.

Async Endpoints and Dependency Injection — Where FastAPI Really Shines

Use async def when your endpoint does I/O — database queries, HTTP calls to external APIs, reading files. These operations spend most of their time waiting, not computing. With async def, FastAPI can handle other requests during that wait instead of blocking a thread. Use plain def when your endpoint does CPU-heavy work — image processing, complex calculations. FastAPI runs those in a thread pool automatically.

Dependency Injection (DI) is FastAPI's answer to sharing reusable logic. You write a function that produces a value, declare it with Depends(), and FastAPI calls it automatically before your endpoint runs.

io/thecodeforge/basics/async_di.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import httpx
from fastapi import FastAPI, Depends, Header, HTTPException
from typing import Annotated

app = FastAPI(title="Async + DI Demo")

def require_api_key(x_api_key: Annotated[str | None, Header()] = None) -> str:
    if x_api_key != "forge-secret-key-123":
        raise HTTPException(status_code=403, detail="Invalid API key")
    return x_api_key

@app.get("/external-quote")
async def fetch_random_quote(api_key: Annotated[str, Depends(require_api_key)]) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get("https://api.quotable.io/random")
    
    if response.status_code != 200:
        raise HTTPException(status_code=502, detail="Upstream failure")
    
    data = response.json()
    return {"quote": data.get("content"), "author": data.get("author")}
Output
# Endpoint validates header and fetches data asynchronously without blocking the server.
Interview Gold: async def vs def in FastAPI
FastAPI handles both correctly, but for different reasons. async def runs on the event loop — perfect for awaitable I/O. Plain def runs in a separate thread pool — FastAPI does this automatically to prevent blocking. The mistake is using plain def with a synchronous database driver that blocks for 200ms per query: you'll saturate the thread pool under load.
Production Insight
Using requests library inside async def blocks the event loop — your async endpoint becomes synchronous.
Monitor thread pool size: default is 40 threads per worker. Saturating it causes request queuing.
Rule: never mix synchronous I/O calls inside async endpoints — use httpx.AsyncClient, aiosqlite, etc.
Key Takeaway
async def for I/O, plain def for CPU — FastAPI handles both automatically.
Depends() turns a plain function into a reusable dependency — no class boilerplate.
The biggest production mistake with FastAPI is blocking the event loop with sync I/O.

Error Handling and Custom Exception Handlers

FastAPI automatically returns proper HTTP responses for validation errors (422) and server errors (500). But for business logic — like 'user not found' or 'insufficient funds' — you need custom error handling. FastAPI lets you raise HTTPException with any status code and detail message. You can also register custom exception handlers to format errors consistently across your API.

A common pattern is to define a custom exception class, then write a handler that catches it and returns a consistent JSON structure. This keeps your endpoint code clean and your error responses uniform — something clients will thank you for.

io/thecodeforge/basics/error_handling.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI(title="Custom Errors")

class InsufficientFundsError(Exception):
    def __init__(self, balance: float, needed: float):
        self.balance = balance
        self.needed = needed

@app.exception_handler(InsufficientFundsError)
async def insufficient_funds_handler(request: Request, exc: InsufficientFundsError):
    return JSONResponse(
        status_code=402,
        content={
            "error": "insufficient_funds",
            "balance": exc.balance,
            "needed": exc.needed,
            "message": f"Need {exc.needed} but only have {exc.balance}"
        }
    )

@app.get("/withdraw/{amount}")
async def withdraw(amount: float):
    balance = 100.0
    if amount > balance:
        raise InsufficientFundsError(balance=balance, needed=amount)
    return {"withdrawn": amount, "remaining": balance - amount}
Output
# Requesting /withdraw/150 returns 402 with a structured error payload.
Think of Exception Handlers as Middleware for Errors
  • FastAPI catches your custom exception, then calls the registered handler.
  • You control the response status code, headers, and body.
  • Handlers can be async — perfect for logging to external systems.
  • Always register handlers before defining routes to avoid import order issues.
Production Insight
Unhandled exceptions return a generic 500 with no detail — bad for debugging and security.
Always register a global exception handler that logs full traceback and returns a safe message.
Rule: never leak stack traces in production responses; log them internally and return a correlation ID.
Key Takeaway
HTTPException is for simple cases; custom exception handlers for consistent error payloads.
A global handler prevents information leakage and improves debuggability.
Always return a structured error object — clients will parse it reliably.

Testing Your FastAPI Application with TestClient

FastAPI comes with a built-in testing utility, TestClient, based on httpx. It lets you send requests to your app without running a server — perfect for unit tests and integration tests. You can test all endpoints, including those with dependencies, by overriding dependencies using app.dependency_overrides.

Write tests for success cases, validation failures, and custom errors. Use pytest as the test runner — it integrates seamlessly. Always use with TestClient(app) as a context manager to ensure proper resource cleanup.

io/thecodeforge/basics/test_book_api.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from fastapi.testclient import TestClient
from io.thecodeforge.basics.book_api import app

client = TestClient(app)

def test_get_book_by_id():
    response = client.get("/books/1")
    assert response.status_code == 200
    data = response.json()
    assert data["title"] == "Clean Code"

def test_get_book_not_found():
    response = client.get("/books/999")
    assert response.status_code == 404
    assert "not found" in response.json()["detail"]

def test_create_book_invalid_year():
    response = client.post("/books", json={
        "title": "Bad", "author": "Me", "year": 500
    })
    assert response.status_code == 422
    errors = response.json()["detail"]
    assert any(err["loc"] == ["body", "year"] for err in errors)

def test_list_books_with_genre_filter():
    response = client.get("/books?genre=Software Engineering&limit=1")
    assert response.status_code == 200
    data = response.json()
    assert len(data) == 1
Output
# Run tests with: pytest test_book_api.py -v
Pro Tip: Override Dependencies for Testing
Use app.dependency_overrides[my_dependency] = test_override to swap out real dependencies (like database sessions) with mocks. Don't forget to call app.dependency_overrides.clear() after each test.
Production Insight
TestClient does not run the ASGI server — it's fast and catches logic errors early.
But it won't detect production-only issues like Uvicorn timeouts or async deadlocks.
Rule: combine TestClient unit tests with end-to-end integration tests against a real server in CI.
Key Takeaway
TestClient lets you test your entire app without running a server.
Override dependencies to isolate the code you're testing.
FastAPI + pytest = fast, reliable tests that catch validation and logic errors.
● Production incidentPOST-MORTEMseverity: high

The 422 Mystery: Why Your Valid Endpoint Rejected a Valid Request

Symptom
All POST requests to /items/ returned 422 Unprocessable Entity even though the JSON body looked correct. No error in logs beyond 422.
Assumption
The client was sending malformed data. The frontend team insisted they validated the payload correctly.
Root cause
The Pydantic model used an alias for a field (e.g., item_name mapped to name), but the client sent the original field name. FastAPI's validation expected the alias, not the original name. The 422 response detailed the field mismatch but the frontend team didn't parse it.
Fix
Updated the model documentation and the frontend to use the correct alias. Added a test that sends both original and aliased names to catch such mismatches early.
Key lesson
  • Aliases are invisible to consumers — always include clear examples in your OpenAPI docs.
  • Log the full validation error body in production to debug 422s faster.
Production debug guideSymptom → Action for the most frequent production pitfalls3 entries
Symptom · 01
Endpoint returns 422 Unprocessable Entity for a supposedly valid payload
Fix
Check the response body for field-level errors. Open /docs and test with the Swagger UI — it shows you exactly which field failed validation and why.
Symptom · 02
Route /books/featured returns 404 or returns wrong data
Fix
Check the order of route definitions. Path parameters like /books/{book_id} must come AFTER static routes like /books/featured, because FastAPI matches top-down.
Symptom · 03
Async endpoint is slow under concurrent requests
Fix
Look for synchronous calls inside async def, especially requests.get(). Replace with httpx.AsyncClient and use await. Use docker stats or equivalent to see thread pool saturation.
★ FastAPI Troubleshooting Cheat SheetThree common errors and immediate fixes
422 Unprocessable Entity on POST
Immediate action
Open /docs and test the request there — it highlights the exact violating field.
Commands
Fix now
Compare your JSON structure with the generated schema. Missing required fields? Wrong type? Use pydantic's model_json_schema() locally to debug.
404 when hitting a valid route+
Immediate action
Run the app with `--reload` and check the console for startup logs — they list all registered routes.
Commands
uvicorn app:app --reload --log-level debug
Fix now
Move static routes above dynamic ones in order.
Server becomes unresponsive under load+
Immediate action
Check if any async endpoint uses `requests.get()` (synchronous). Replace with `httpx.AsyncClient`.
Commands
grep -rn "import requests" app/
grep -rn "async def" app/ | grep -v "await"
Fix now
Refactor to async. If you must keep sync endpoints, ensure they're short and CPU-bound.
FastAPI vs Flask vs Django REST Framework
Feature / AspectFastAPIFlaskDjango REST Framework
Server typeASGI (async-native)WSGI (sync-native)WSGI (sync-native, async experimental)
Data validationAutomatic via Pydantic type hintsManual — you write it yourselfManual — via serializers (verbose)
API documentationAuto-generated Swagger + ReDocNone — requires extra libraries (like flasgger)Auto-generated through DRF-YASG (extra lib)
Performance (I/O bound)Comparable to Node.js / GoSlower thread-per-request modelSlower due to WSGI and heavy middleware
Learning curveSlightly steeper (Types, Pydantic)Gentler for total beginnersSteepest — requires Django + DRF knowledge
Built-in DIYes — Depends()No (manual injection or Flask-Injector)No (manual, class-based views help)

Key takeaways

1
FastAPI reads your Python type hints at startup
that single act powers automatic request validation, response serialization, and interactive Swagger docs all at once. You write types once, you get three things free.
2
Path parameters identify a specific resource (/users/42), query parameters filter a collection (/users?role=admin), and request bodies carry complex structured data.
3
Use async def for endpoints that do I/O (database, HTTP calls) and plain def for CPU work
FastAPI runs plain def in a thread pool automatically.
4
Dependency Injection via Depends() is how FastAPI handles authentication, database sessions, and shared config cleanly
it keeps your endpoints thin and your shared logic testable in one place.
5
Custom exception handlers let you return consistent error payloads across your API
don't rely on generic 500s.
6
Test your endpoints with FastAPI's TestClient to catch validation and logic errors early
it runs without a server and integrates with pytest.

Common mistakes to avoid

3 patterns
×

Using `requests` inside an async endpoint

Symptom
Your async endpoint works but blocks the event loop during every HTTP call. Under concurrent load, request latency spikes and eventually the server stops responding.
Fix
Replace requests with httpx.AsyncClient and await the call. Example: async with httpx.AsyncClient() as client: response = await client.get(url)
×

Forgetting to return the correct HTTP status code for POST requests

Symptom
Your create endpoint returns 200 OK instead of 201 Created. Clients that check status codes (e.g., frontend redirect logic) may behave incorrectly.
Fix
Add status_code=201 to your @app.post() decorator. Also ensure you return a response with a Location header pointing to the new resource.
×

Mutating a Pydantic model's default mutable argument

Symptom
Data leaks between requests. If you define a model field with default=[], every request that doesn't provide that field shares the same list object.
Fix
Use default_factory=list instead of default=[]. Pydantic creates a fresh list for each request. Same for dicts: default_factory=dict.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the 'Starlette + Pydantic' architecture of FastAPI. How do these...
Q02SENIOR
A client reports that your `/users/{user_id}` endpoint is returning a 42...
Q03SENIOR
Describe a scenario where using `async def` would actually be *slower* t...
Q04SENIOR
How does the `Depends` system handle 'Sub-dependencies'? If Dependency A...
Q05SENIOR
What is the role of `uvicorn` or `hypercorn` in a FastAPI deployment, an...
Q01 of 05SENIOR

Explain the 'Starlette + Pydantic' architecture of FastAPI. How do these two libraries divide the work of handling a request?

ANSWER
Starlette handles the low-level ASGI communication: routing, request parsing, response streaming, middleware, and WebSocket support. Pydantic handles data validation and serialization via Python type hints. When a request arrives, Starlette extracts path, query, and body parameters based on the route definition. FastAPI then passes the raw data types (e.g., path param as string) through Pydantic models for validation, type coercion, and nested model construction. The validated objects are injected into your endpoint. After the endpoint returns, Pydantic serializes the response model back to JSON, and Starlette sends it over the wire. FastAPI orchestrates the two — it's a thin integration layer.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Do I need to know async/await to use FastAPI?
02
What is Pydantic and why does FastAPI depend on it?
03
What is the difference between FastAPI's automatic 422 error and a 400 Bad Request?
04
How can I deploy a FastAPI application to production?
05
How do I handle CORS in FastAPI?
🔥

That's Python Libraries. Mark it forged?

3 min read · try the examples if you haven't

Previous
threading and multiprocessing in Python
16 / 51 · Python Libraries
Next
Celery for Task Queues in Python