FastAPI Basics: Build Your First Python API in Minutes
Master FastAPI fundamentals: routing, Pydantic validation, and dependency injection.
- 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.
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.
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.uvicorn app:app without --reload before deploying.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.
/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.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.
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.requests library inside async def blocks the event loop — your async endpoint becomes synchronous.Depends() turns a plain function into a reusable dependency — no class boilerplate.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.
- 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.
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.
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.The 422 Mystery: Why Your Valid Endpoint Rejected a Valid Request
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.- 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.
requests.get(). Replace with httpx.AsyncClient and use await. Use docker stats or equivalent to see thread pool saturation.model_json_schema() locally to debug.Key takeaways
/users/42), query parameters filter a collection (/users?role=admin), and request bodies carry complex structured data.async def for endpoints that do I/O (database, HTTP calls) and plain def for CPU workdef in a thread pool automatically.Depends() is how FastAPI handles authentication, database sessions, and shared config cleanlyTestClient to catch validation and logic errors earlyCommon mistakes to avoid
3 patternsUsing `requests` inside an async endpoint
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
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
default=[], every request that doesn't provide that field shares the same list object.default_factory=list instead of default=[]. Pydantic creates a fresh list for each request. Same for dicts: default_factory=dict.Interview Questions on This Topic
Explain the 'Starlette + Pydantic' architecture of FastAPI. How do these two libraries divide the work of handling a request?
Frequently Asked Questions
That's Python Libraries. Mark it forged?
3 min read · try the examples if you haven't