FastAPI has quickly become the go‑to framework for building high‑performance REST APIs in Python. Its automatic OpenAPI docs, type‑driven validation, and async‑first design make it a joy to work with—whether you’re prototyping a hobby project or powering a production microservice. In this guide we’ll walk through everything you need to spin up a fully functional REST API, from environment setup to containerised deployment. By the end you’ll have a clean, testable codebase you can extend with authentication, databases, or background tasks.
What You’ll Need
- Python 3.9+ installed on your workstation
- Basic knowledge of Python functions and classes
- A terminal or command prompt with administrative rights
- Git (optional, for version control)
- Docker (optional, for containerisation)
Step 1: Set Up a Virtual Environment
Isolating your project dependencies prevents version clashes with other Python apps. Open a terminal and run:
mkdir fastapi-demo && cd fastapi-demo
python -m venv venv
# On Windows
venvScriptsactivate
# On macOS/Linux
source venv/bin/activate
The prompt should now show (venv), indicating the environment is active. Remember to deactivate with deactivate when you’re done.
Step 2: Install FastAPI and an ASGI Server
FastAPI itself is just a library; you need an ASGI server like uvicorn to run it. Install both with pip:
pip install fastapi uvicorn[standard]
The [standard] extra pulls in useful packages such as watchgod for auto‑reloading during development.
Step 3: Create Your First Endpoint
Create a file named main.py and add a minimal API:
from fastapi import FastAPI
app = FastAPI()
@app.get("/ping")
async def ping() -> dict:
return {"message": "pong"}
Run it with:
uvicorn main:app --reload
The --reload flag watches for file changes and restarts automatically. Open http://127.0.0.1:8000/ping in a browser—you should see {"message":"pong"}. FastAPI also generates interactive docs at /docs (Swagger UI) and /redoc.
Step 4: Add Data Validation with Pydantic Models
One of FastAPI’s strengths is automatic request validation using Pydantic. Define a model for a simple Item resource:
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(..., example="Widget")
price: float = Field(..., gt=0, description="Price in USD")
tags: list[str] | None = None
Now create POST and GET endpoints that use this model:
from fastapi import HTTPException
fake_db: dict[int, Item] = {}
@app.post("/items", response_model=Item, status_code=201)
async def create_item(item: Item) -> Item:
item_id = len(fake_db) + 1
fake_db[item_id] = item
return item
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: int) -> Item:
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
Try it with curl:
curl -X POST "http://127.0.0.1:8000/items"
-H "Content-Type: application/json"
-d '{"name":"Gadget","price":19.99}'
The response will be the created item, and you can retrieve it via /items/1. FastAPI will automatically reject malformed JSON or missing fields, returning a clear 422 error.
Step 5: Organise the Project Structure
As your API grows, keep the code tidy by separating routers, models, and services. A common layout looks like this:
fastapi-demo/
├─ app/
│ ├─ __init__.py
│ ├─ main.py # FastAPI instance
│ ├─ models.py # Pydantic schemas
│ ├─ routers/
│ │ └─ items.py # Item‑related routes
│ └─ core/
│ └─ config.py # Settings (env vars, etc.)
└─ tests/
└─ test_items.py
Example app/routers/items.py:
from fastapi import APIRouter, HTTPException
from ..models import Item
router = APIRouter(prefix="/items", tags=["items"])
_fake_db: dict[int, Item] = {}
@router.post("", response_model=Item, status_code=201)
async def create_item(item: Item) -> Item:
item_id = len(_fake_db) + 1
_fake_db[item_id] = item
return item
@router.get("/{item_id}", response_model=Item)
async def read_item(item_id: int) -> Item:
if item_id not in _fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return _fake_db[item_id]
And in app/main.py you include the router:
from fastapi import FastAPI
from .routers import items
app = FastAPI(title="FastAPI Demo")
app.include_router(items.router)
This modular approach makes unit testing easier and keeps the entry point clean.
Step 6: Containerise with Docker
Deploying a FastAPI app is straightforward with Docker. Create a Dockerfile at the project root:
# Use an official lightweight Python image
FROM python:3.11-slim
# Set a working directory
WORKDIR /app
# Install dependencies first for caching
COPY pyproject.toml poetry.lock ./
RUN pip install --no-cache-dir poetry &&
poetry config virtualenvs.create false &&
poetry install --no-dev
# Copy the source code
COPY . .
# Expose the default FastAPI port
EXPOSE 8000
# Run the server
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Build and run:
docker build -t fastapi-demo .
docker run -p 8000:8000 fastapi-demo
The API is now reachable at http://localhost:8000. Using Docker ensures the same runtime environment across development, staging, and production.
Common Mistakes to Avoid
1. Blocking code in async endpoints. FastAPI expects async def handlers to be non‑blocking. Calling a long‑running CPU‑bound function directly will stall the event loop. Use run_in_threadpool or move the work to a background worker (e.g., Celery).
2. Returning mutable default arguments. Defining a Pydantic field like tags: list[str] = [] shares the same list across requests. Always use Field(default_factory=list).
3. Missing type hints. FastAPI derives request validation from type annotations. Forgetting a hint (e.g., using def foo(param):) disables validation and can cause obscure bugs.
4. Hard‑coding configuration. Secrets, database URLs, and debug flags belong in environment variables or a .env file, not in source code.
5. Not testing the OpenAPI schema. Changes to models may not reflect in the generated docs if you forget to restart the server when using --reload in production mode.
Tips and Tricks
– Use Dependency Injection. Common resources like DB sessions or authentication can be injected with Depends, keeping route functions tidy.
– Leverage the built‑in ReDoc. The /redoc endpoint provides a clean, printable API reference.
– Enable CORS early. If your frontend runs on a different origin, add from fastapi.middleware.cors import CORSMiddleware and configure allowed origins.
– Run linting and type‑checking. Tools like ruff and mypy catch subtle bugs before they hit production.
– Profile with uvicorn --log-level debug. It shows request latency and helps spot slow endpoints.
Frequently Asked Questions
Do I need to use async functions for every endpoint?
No. You can mix def (sync) and async def (async) routes. Use async only when you call non‑blocking I/O (database drivers, HTTP clients). Sync functions are fine for CPU‑light work.
How does FastAPI compare to Flask or Django REST Framework?
FastAPI shines in performance (thanks to Starlette and ASGI) and automatic validation/documentation. Flask is more minimal but requires extra extensions for OpenAPI. Django REST Framework offers a full‑stack solution with ORM integration, but its request‑response cycle is slower and less suited for pure async workloads.
Can I add authentication to this API?
Absolutely. FastAPI supports OAuth2, JWT, API keys, and third‑party providers. The typical pattern is to create a security dependency that validates the token and injects the current user into your route functions.
Conclusion
FastAPI makes building a production‑ready REST API in Python feel almost effortless. By following the steps above—setting up a virtual environment, defining Pydantic models, organising routers, and optionally containerising with Docker—you’ll have a solid foundation that scales from a quick prototype to a microservice in a cloud‑native architecture. Remember to respect async best practices, keep configuration external, and write tests early. Happy coding, and enjoy the speed and developer experience FastAPI brings to your next project!
Photo by Portraits of Modern Dairy on Unsplash





