FastAPI Basics: Build Your First Python API in Minutes
Master FastAPI fundamentals: routing, Pydantic validation, and dependency injection.
20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.
- 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)
What FastAPI Actually Does for Python APIs
FastAPI is a modern Python web framework that builds REST APIs using Python type hints. Its core mechanic: you declare request parameters, query strings, and body schemas as standard Python type annotations, and FastAPI automatically handles validation, serialization, and OpenAPI documentation generation. This eliminates boilerplate validation code and keeps your endpoint logic clean.
Under the hood, FastAPI leverages Starlette for async request handling and Pydantic for data validation. Every endpoint can be synchronous or asynchronous — the framework runs sync functions in a threadpool and async functions on the event loop. This means you get automatic request validation (e.g., int vs str), response serialization (dict to JSON), and interactive docs at /docs — all from type hints alone. Performance is comparable to Node.js or Go for I/O-bound workloads because of async support.
Use FastAPI when you need a Python API that must handle high concurrency (e.g., microservices, real-time data pipelines) or when you want to minimize time-to-documentation. It shines in systems where schema changes are frequent — type hints act as a single source of truth for both validation and docs. Avoid it for CPU-bound endpoints unless you offload to a task queue.
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.Why You Need Environment and Config Management from Day One
I've seen projects crumble because someone hardcoded a database URL into the code. FastAPI's BaseSettings from Pydantic makes this literally a one-liner — and it's the only way to survive production. You define a class that inherits from BaseSettings, declare your environment variables with type hints, and FastAPI automatically loads them from .env files, environment variables, or both. This isn't a nice-to-have; it's the difference between a deploy that works and a 3 AM page that the database is unreachable. The WHY is simple: secrets change between environments. Your local Postgres URL is not your staging RDS endpoint. By centralizing config, you eliminate an entire class of 'works on my machine' bugs. Do this before your first endpoint. Future you — and the on-call engineer — will thank you.
Middleware: The Layer That Catches Silent Failures
Middleware is code that runs before every request and after every response. Most devs skip it until they need CORS headers or request logging. That's a mistake. Here's the WHY: middleware lets you enforce cross-cutting concerns without touching a single endpoint. Want to log every request's duration? Middleware. Need to block requests from a specific IP range? Middleware. Want to add a security header like HSTS? Middleware. FastAPI's middleware is just a callable that takes a request and a call_next function. You wrap the call in time tracking, add headers, or abort early. This keeps your route handlers clean and your security consistent. Forget to add CORS middleware and your frontend dev will curse you. Neglect to log slow requests and you'll never know which endpoint is degrading. Add middleware before your first deploy — not after your first outage.
Background Tasks: Don't Block the User for Work That Can Wait
Here's a scenario: a user uploads a CSV, and you need to process it, send a welcome email, and update an analytics dashboard. If you do all that synchronously, the API returns in 30 seconds. The user thinks it's broken. FastAPI gives you BackgroundTasks — a dead-simple way to push work into a background thread or async task after the response is sent. The WHY is user experience. The HTTP response should be fast. Everything else — file processing, email sending, cache warming — can happen in the background. You inject a BackgroundTasks parameter into your endpoint, call tasks.add_task(your_function, arg1, arg2), and return immediately. The task runs after the response. This isn't a full job queue like Celery. For that, you need Redis or RabbitMQ. But for simple, fire-and-forget operations, BackgroundTasks is your best friend. Use it. Your UI will feel snappy, and your users will stay happy.
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
20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.
That's Python Libraries. Mark it forged?
6 min read · try the examples if you haven't