Building a REST API with FastAPI: A Practical Starter
If you're building the backend for a mobile or web app in Python, FastAPI is one of the best places to start. It's modern, fast, and gives you request validation and interactive documentation almost for free. This post walks through the core ideas and the practices that keep a FastAPI project healthy as it grows.
Why FastAPI
Three things make FastAPI stand out:
- Type-driven validation. You declare what your data looks like with Python type hints, and FastAPI validates incoming requests against them automatically. Bad requests get clear errors before your code ever runs.
- Automatic documentation. It generates interactive API docs from your code, so you (and your app's client) always have an accurate, testable reference.
- Async support. It's built on modern async Python, so it handles many simultaneous connections efficiently — a real advantage for an API backing a busy app.
A minimal API
Here's a complete, working endpoint:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
Run it with an ASGI server (uvicorn main:app --reload) and you have a live API with auto-generated docs. That's the whole "hello world."
Validation with models
The real power shows up when you define the shape of your data using Pydantic models. FastAPI uses them to validate input and to document your API:
from pydantic import BaseModel
class Expense(BaseModel):
title: str
amount: float
category: str | None = None
@app.post("/expenses")
def create_expense(expense: Expense):
# `expense` is already validated and typed here
saved = save_to_db(expense)
return {"id": saved.id}
If a client sends a missing field or the wrong type, FastAPI rejects the request with a helpful error automatically. You spend your time on logic, not on hand-writing validation.
Async when it helps
For endpoints that wait on I/O — a database, another API, an LLM call — declare them async and use await, so the server can handle other requests while yours waits:
@app.get("/summary")
async def summary():
data = await fetch_from_db() # non-blocking
return {"summary": data}
Use async for I/O-bound work; plain def is perfectly fine for simple, fast handlers. Don't put slow, blocking calls inside an async function — that stalls the whole event loop.
Structure it before it sprawls
A single main.py is great for a demo and painful for a real app. As soon as you have more than a handful of endpoints, organize:
- Routers — group related endpoints (
expenses,users) into separate modules and include them in the app. - Schemas — keep your Pydantic models together, separate from your route logic.
- Services — put business logic in its own layer, so routes stay thin and logic stays testable.
- Dependencies — use FastAPI's dependency injection for shared concerns like database sessions and authentication.
This separation keeps each file small and makes the project easy to navigate months later.
Don't skip these
A few things that separate a toy from a shippable API:
- Authentication. Protect endpoints that need it. FastAPI's dependency system makes it clean to require a valid token on protected routes.
- Error handling. Return meaningful HTTP status codes and consistent error bodies your client can rely on.
- Environment configuration. Keep secrets (database URLs, API keys) in environment variables, never in code.
- CORS. If a browser app calls your API, configure Cross-Origin Resource Sharing correctly or requests will be blocked.
- Tests. FastAPI has a built-in test client — write tests for your endpoints early; they pay for themselves quickly.
Summary
FastAPI lets you build a modern Python backend quickly without sacrificing rigor: type hints give you automatic validation and documentation, and async support keeps it efficient under load. Start with simple typed endpoints, model your data with Pydantic, use async for I/O-bound work, and organize into routers, schemas, and services before the code sprawls. Add authentication, sensible error handling, and tests, and you've got a backend that's pleasant to build on and safe to grow.