FastAPI from Scratch: Setup, Type Hints and OpenAPI Self-Documentation
Learn how to start a FastAPI project in less than 5 minutes: type hints as API contract, Pydantic v2 for data validation and automatically generated Swagger documentation without extra configuration.
Why FastAPI in 2026
FastAPI has scaled from an experimental project to a reference framework for APIs in just a few years Python in production. According to the Python Developers Survey 2024, it is the second framework Python web by diffusion (38%), surpassed only by Django. The reasons are concrete: performance comparable to Node.js and Go for I/O-bound workloads, zero-boilerplate data validation with Pydantic v2, and OpenAPI documentation automatically generated from the code.
The secret is architectural: FastAPI is built on Starlets (ASGI framework) e Pydantic (validation with Rust core). It doesn't add useless abstractions — every feature has a precise technical reason.
What You Will Learn
- Installation and first FastAPI server running in 5 minutes
- How Python type hints become validation, serialization, and documentation
- Path parameters, query parameters and request bodies with Pydantic
- Swagger UI and ReDoc: Navigate automatically generated documentation
- Status codes, response models and basic error handling
- Structure of a real FastAPI project (not just a single file)
Installation and First Server
FastAPI requires Python 3.8+ but we recommend 3.11+ for improved performance of asyncio and the new features of type hints. Uvicorn is the ASGI server recommended for local development.
# Ambiente virtuale (sempre, mai installare globalmente)
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate # Windows
# Installazione dipendenze core
pip install fastapi uvicorn[standard]
# Per sviluppo aggiungere anche:
pip install httpx pytest pytest-asyncio
# Verifica installazione
python -c "import fastapi; print(fastapi.__version__)"
# 0.115.x
The minimum possible server. Save this as main.py:
# main.py - Il server FastAPI piu semplice possibile
from fastapi import FastAPI
# Crea l'istanza dell'applicazione
# title e description appaiono nella documentazione Swagger
app = FastAPI(
title="La Mia API",
description="API costruita con FastAPI e Python type hints",
version="1.0.0",
)
# Decorator che registra la route GET /
@app.get("/")
def read_root():
# Il dict viene automaticamente serializzato in JSON
return {"message": "Hello, FastAPI!", "status": "running"}
# Avvio con: uvicorn main:app --reload
# main = nome del file, app = variabile FastAPI, --reload = hot reload
# Avvia il server con hot reload (ricrea il server ad ogni modifica)
uvicorn main:app --reload
# Output:
# INFO: Will watch for changes in these directories: ['/path/to/project']
# INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
# INFO: Started reloader process [12345]
# INFO: Started server process [12346]
# INFO: Waiting for application startup.
# INFO: Application startup complete.
Open http://127.0.0.1:8000/docs to see the generated Swagger UI
automatically. You haven't configured anything: FastAPI has read the code annotations
and generated complete documentation.
Type Hints as API Contract
The most powerful feature of FastAPI is the use of Python type hints such as API contract definition. Each annotated parameter automatically becomes: validated, documented, and serialized. Zero additional boilerplate code.
# routes/users.py - Esempi di parametri con type hints
from fastapi import FastAPI, Path, Query, HTTPException
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
app = FastAPI()
# --- PATH PARAMETERS ---
# Il tipo int fa si che FastAPI validi che userId sia un intero
@app.get("/users/{user_id}")
def get_user(
user_id: int, # Path parameter: automaticamente estratto dall'URL
):
if user_id <= 0:
raise HTTPException(status_code=400, detail="user_id must be positive")
return {"user_id": user_id, "name": f"User {user_id}"}
# Path con validazione avanzata tramite Path()
@app.get("/items/{item_id}")
def get_item(
item_id: int = Path(
title="The ID of the item",
description="Must be a positive integer",
ge=1, # greater than or equal to 1
le=1000, # less than or equal to 1000
),
):
return {"item_id": item_id}
# --- QUERY PARAMETERS ---
# Parametri con default = opzionali, senza default = obbligatori
@app.get("/search")
def search_users(
q: str, # Obbligatorio (no default)
page: int = 1, # Opzionale con default
limit: int = Query(default=10, ge=1, le=100), # Con validazione
active_only: bool = True, # Bool viene da "true"/"false" nella query string
role: Optional[str] = None, # Opzionale, None se non fornito
):
return {
"query": q,
"page": page,
"limit": limit,
"active_only": active_only,
"role": role,
}
# GET /search?q=mario&page=2&limit=20&active_only=false&role=admin
Request Body with Pydantic Models
For data in the body of a POST/PUT/PATCH request, Pydantic templates are used. FastAPI automatically deserializes the incoming JSON, validates it against the template, and makes it available as a typed Python object.
# models/user.py - Definizione dei modelli con Pydantic v2
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional
from enum import Enum
class UserRole(str, Enum):
admin = "admin"
editor = "editor"
viewer = "viewer"
# Modello per la creazione di un utente (in input)
class UserCreate(BaseModel):
name: str = Field(
min_length=2,
max_length=100,
description="Full name of the user",
examples=["Mario Rossi"],
)
email: EmailStr # Validazione email inclusa in Pydantic
role: UserRole = UserRole.viewer # Default al valore meno privilegiato
age: Optional[int] = Field(default=None, ge=0, le=150)
# Validator personalizzato (Pydantic v2 syntax)
@field_validator("name")
@classmethod
def name_must_contain_space(cls, v: str) -> str:
if " " not in v:
raise ValueError("name must contain at least first and last name")
return v.strip()
# Modello per la risposta (include campi generati dal server)
class UserResponse(BaseModel):
id: int
name: str
email: EmailStr
role: UserRole
created_at: str # ISO 8601
# Route POST che usa i modelli
@app.post(
"/users",
response_model=UserResponse, # Definisce la struttura della risposta
status_code=201, # HTTP 201 Created
summary="Create a new user",
tags=["users"],
)
def create_user(user: UserCreate):
# user e gia validato e typed come UserCreate
# FastAPI ha deserializzato il JSON e verificato tutti i vincoli
new_user = {
"id": 42, # In realta verrebbe dal database
"name": user.name,
"email": user.email,
"role": user.role.value,
"created_at": "2026-06-01T10:00:00Z",
}
return new_user
# Solo i campi di UserResponse vengono inclusi nella risposta
# (response_model filtra automaticamente campi extra come password hash)
Error Handling with HTTPException
FastAPI maps exceptions into structured HTTP responses. The standard pattern uses
HTTPException for expected errors and a global exception handler for
unexpected errors.
# Errori standard con HTTPException
from fastapi import HTTPException, status
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
user = db.find_user(user_id) # Ipotetica funzione DB
if user is None:
# status.HTTP_404_NOT_FOUND = 404 (uso le costanti, piu leggibile)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with id {user_id} not found",
)
return user
# Exception handler globale per errori non gestiti
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
# Log l'errore (NON esporre il dettaglio in produzione)
import logging
logging.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"},
)
Structure of a Real Project
A single main.py it's fine for tutorials, but not for a real project.
The recommended structure separates concerns into cohesive modules:
# Struttura consigliata per un progetto FastAPI medio
my_api/
├── app/
│ ├── __init__.py
│ ├── main.py # Entry point: crea l'app FastAPI e include i router
│ ├── config.py # Configurazione (env vars, Settings con Pydantic)
│ ├── database.py # Connessione DB, session factory
│ ├── models/
│ │ ├── __init__.py
│ │ ├── user.py # SQLAlchemy ORM models
│ │ └── item.py
│ ├── schemas/ # Pydantic models (request/response)
│ │ ├── __init__.py
│ │ ├── user.py # UserCreate, UserUpdate, UserResponse
│ │ └── item.py
│ ├── routers/ # APIRouter per ogni dominio
│ │ ├── __init__.py
│ │ ├── users.py # /users endpoints
│ │ └── items.py # /items endpoints
│ ├── services/ # Business logic (separata dai router)
│ │ ├── user_service.py
│ │ └── item_service.py
│ └── dependencies.py # Depends() condivise (auth, DB session)
├── tests/
│ ├── conftest.py
│ ├── test_users.py
│ └── test_items.py
├── alembic/ # Migrations DB
├── pyproject.toml
└── docker-compose.yml
# app/main.py - Entry point con router modulari
from fastapi import FastAPI
from app.routers import users, items
from app.config import get_settings
settings = get_settings()
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
docs_url="/docs" if settings.DEBUG else None, # Disabilita docs in produzione
redoc_url="/redoc" if settings.DEBUG else None,
)
# Include i router con prefisso e tag per la documentazione
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(items.router, prefix="/items", tags=["items"])
@app.get("/health", tags=["system"])
def health_check():
return {"status": "healthy", "version": settings.APP_VERSION}
OpenAPI documentation: Swagger and ReDoc
FastAPI automatically generates two interactive documentation interfaces for you to visit during development:
-
Swagger UI (
/docs): interactive interface for testing the API directly from the browser. Supports authentication, JSON body, query parameters. -
ReDoc (
/redoc): readable documentation, ideal for share with your team or API consumers. -
OpenAPI JSON (
/openapi.json): machine-readable schema used to generate SDK clients in any language.
# Arricchire la documentazione con metadata aggiuntivi
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
app = FastAPI()
# Override del schema OpenAPI per aggiungere security schemes e metadata
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="My API",
version="2.0.0",
description="Descrizione lunga dell'API con **markdown** supportato",
terms_of_service="https://example.com/terms",
contact={
"name": "Federico Calo",
"url": "https://federicocalo.dev",
"email": "info@federicocalo.dev",
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
},
routes=app.routes,
)
# Aggiungi Bearer token authentication
openapi_schema["components"]["securitySchemes"] = {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
}
}
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
Documentation in Production
Always disable Swagger UI and ReDoc in production by setting
docs_url=None e redoc_url=None in the FastAPI constructor.
The interactive documentation exposes the internal structure of the API and can
be used to explore undocumented endpoints. The endpoint
/openapi.json must be protected in the same way.
Configuration with Pydantic Settings
The correct way to handle configuration in FastAPI is to use
pydantic-settings which automatically reads environment variables
with type validation:
# app/config.py - Configurazione type-safe con pydantic-settings
# pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
from functools import lru_cache
class Settings(BaseSettings):
# Valori letti da variabili d'ambiente (case insensitive)
APP_NAME: str = "My FastAPI App"
APP_VERSION: str = "1.0.0"
DEBUG: bool = False
# Database
DATABASE_URL: str # Obbligatorio: fallisce se non presente
DB_POOL_SIZE: int = 10
DB_MAX_OVERFLOW: int = 20
# Secrets
SECRET_KEY: str # Obbligatorio
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# CORS
ALLOWED_ORIGINS: list[str] = ["http://localhost:3000"]
model_config = SettingsConfigDict(
env_file=".env", # Legge da .env se presente
env_file_encoding="utf-8",
case_sensitive=False, # DATABASE_URL = database_url
)
# lru_cache evita di rileggere il file .env ad ogni richiesta
@lru_cache
def get_settings() -> Settings:
return Settings()
# Uso nelle route tramite Depends()
from fastapi import Depends
@app.get("/info")
def app_info(settings: Settings = Depends(get_settings)):
return {
"name": settings.APP_NAME,
"version": settings.APP_VERSION,
"debug": settings.DEBUG,
}
Conclusions and Next Steps
FastAPI combines three features that generally exclude each other: development speed, type safety and performance. Type hints are not decorative — they drive validation, documentation and automatic serialization. This drastically reduces the code boilerplate compared to Flask or Django REST Framework.
The next step is to understand the asynchronous model that makes FastAPI fast for
I/O-bound workloads: how asyncio, event loops and coroutines work, and when to use them
async def vs def normal.
Python FastAPI and Async Web Series
- Article 1 (this): Setup, Type Hints and OpenAPI Self-Documentation
- Article 2: Async/Await in Python: Event Loops, Coroutines, and I/O-Bound Concurrency
- Article 3: Pydantic v2: Advanced Validation, BaseModel and TypeAdapter
- Article 4: Dependency Injection: Pattern for Clean and Testable Code
- Article 5: Async Database with SQLAlchemy 2.0 and Alembic







