Application Creation
Creating a FastAPI app is one line of code — but those options matter. Learn what FastAPI() does under the hood and how to configure your application with metadata.
FastAPI()
FastAPI() creates the central application object. It's a class that inherits from Starlette (the ASGI framework underneath FastAPI) and adds automatic validation, serialization, and docs generation on top.
FastAPI() is like opening a restaurant. The moment you open it, you get a building (ASGI server), a front-desk (request routing), a menu board (OpenAPI docs), and a kitchen (your endpoint functions). Everything is set up by that one call.
from fastapi import FastAPI # Create the app — this is the ASGI application app = FastAPI() # Define an endpoint (route) @app.get("/") async def root(): return {"message": "Hello World"} # Run it: uvicorn main:app --reload # Visit: http://localhost:8000 → {"message": "Hello World"} # Docs: http://localhost:8000/docs → Swagger UI (FREE!) # Redoc: http://localhost:8000/redoc → ReDoc UI (also FREE!)
Pass metadata to FastAPI() to customize your API's documentation page. All of these show up in the auto-generated Swagger UI and OpenAPI schema.
from fastapi import FastAPI app = FastAPI( # Shown at top of Swagger UI title="My Awesome API", # Shown below the title — supports Markdown! description=""" ## My Awesome API This API does **amazing things**. ### Features - 🚀 Fast - 🔒 Secure - 📖 Auto-documented """, # Shown in the top-right of Swagger UI version="1.0.0", # Contact info for your API consumers contact={ "name": "Alice Dev", "email": "alice@example.com", "url": "https://example.com", }, # License info license_info={ "name": "MIT", "url": "https://opensource.org/licenses/MIT", }, # Terms of service URL terms_of_service="https://example.com/terms", # Custom docs URL (default: /docs) docs_url="/api/docs", # Custom redoc URL (default: /redoc) redoc_url="/api/redoc", # Custom OpenAPI schema URL (default: /openapi.json) openapi_url="/api/openapi.json", # Disable docs entirely in production: # docs_url=None, redoc_url=None ) @app.get("/") async def root(): return {"status": "ok"}
None to disable.None to disable.docs_url=None, redoc_url=None) so external users can't explore your API schema.Lifespan
Run code on startup (load ML models, open DB connections) and on shutdown (close connections, flush buffers) — cleanly and reliably.
Startup & Shutdown
The modern way to handle startup and shutdown in FastAPI is the lifespan context manager (FastAPI 0.93+). Everything before yield = startup. Everything after = shutdown.
from contextlib import asynccontextmanager from fastapi import FastAPI # Simulating a database connection pool db_pool = {} @asynccontextmanager async def lifespan(app: FastAPI): # ── STARTUP ── (runs before first request) print("🚀 Starting up...") db_pool["conn"] = await create_db_connection() print("✅ Database connected!") yield # ← app runs here, handling requests # ── SHUTDOWN ── (runs after last request) print("🛑 Shutting down...") await db_pool["conn"].close() print("✅ Database connection closed.") # Pass lifespan to the app app = FastAPI(lifespan=lifespan) @app.get("/") async def root(): conn = db_pool["conn"] # available everywhere! return {"status": "ready"} async def create_db_connection(): return {"host": "localhost"}
Common things to initialize on startup and clean up on shutdown:
from contextlib import asynccontextmanager from fastapi import FastAPI import httpx # Global state shared across requests state = {} @asynccontextmanager async def lifespan(app: FastAPI): # ── STARTUP ── # 1. Load an ML model (expensive — do it once!) state["model"] = load_model("model.pkl") # 2. Create a shared HTTP client (reuses connections) state["http_client"] = httpx.AsyncClient() # 3. Connect to Redis cache state["cache"] = await connect_redis("redis://localhost") # 4. Warm up the cache await state["cache"].ping() print("✅ All resources initialized") yield # ← requests are handled here # ── SHUTDOWN ── await state["http_client"].aclose() await state["cache"].close() print("✅ All resources cleaned up") app = FastAPI(lifespan=lifespan) @app.get("/predict") async def predict(text: str): model = state["model"] # model loaded at startup ✓ result = model.predict(text) return {"prediction": result}
@app.on_event("startup") and @app.on_event("shutdown") decorators. They still work but the lifespan approach is now preferred as it's cleaner and more Pythonic.Routing
Define routes for each HTTP method, configure them with tags and prefixes, and organize large APIs using APIRouter for clean modular code.
HTTP Methods
FastAPI has decorators for every standard HTTP method. Choose the right one based on what your endpoint does:
| Decorator | Method | Purpose | Has Body? |
|---|---|---|---|
| @app.get | GET | Read/retrieve data | No |
| @app.post | POST | Create a new resource | Yes |
| @app.put | PUT | Replace a resource entirely | Yes |
| @app.patch | PATCH | Update part of a resource | Yes |
| @app.delete | DELETE | Remove a resource | Usually no |
| @app.head | HEAD | Like GET but no response body | No |
| OPTIONS | What methods are allowed (CORS) | No |
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str price: float # GET — retrieve a list @app.get("/items") async def list_items(): return [{"id": 1, "name": "Widget"}] # GET — retrieve one @app.get("/items/{item_id}") async def get_item(item_id: int): return {"id": item_id, "name": "Widget"} # POST — create @app.post("/items", status_code=201) async def create_item(item: Item): return {"id": 42, **item.model_dump()} # PUT — replace @app.put("/items/{item_id}") async def replace_item(item_id: int, item: Item): return {"id": item_id, **item.model_dump()} # PATCH — partial update @app.patch("/items/{item_id}") async def update_item(item_id: int, item: Item): return {"id": item_id, "updated": item.model_dump()} # DELETE @app.delete("/items/{item_id}", status_code=204) async def delete_item(item_id: int): return None # 204 No Content
Route Configuration
Route decorators accept several configuration options that affect how the route appears in docs and how it behaves:
@app.get( "/items/{item_id}", # Groups this endpoint in Swagger UI under "Items" tags=["items"], # Summary shown in the endpoint list summary="Get a single item by ID", # Longer description (supports Markdown) description="Returns the item with the given ID. Raises 404 if not found.", # HTTP status code for the "success" response (default: 200) status_code=200, # Unique name for reverse URL generation name="get_item", # Mark as deprecated in docs (shown with strikethrough) deprecated=False, # Hide from docs entirely include_in_schema=True, ) async def get_item(item_id: int): return {"id": item_id} # You can also use docstrings as the description: @app.get("/users", tags=["users"]) async def list_users(): """ List all active users. Returns a paginated list of users sorted by creation date. """ return []
APIRouter — Modular Routing
Defining all routes on app directly doesn't scale. APIRouter lets you split routes into separate files and include them in the main app — like Express Router or Django's urlconf.
# routers/users.py from fastapi import APIRouter # Create a router (mini-app) router = APIRouter( prefix="/users", # all routes start with /users tags=["users"], # group in Swagger UI under "users" responses={404: {"description": "Not found"}}, ) @router.get("/") # → GET /users/ async def list_users(): return [{"id": 1, "name": "Alice"}] @router.get("/{user_id}") # → GET /users/{user_id} async def get_user(user_id: int): return {"id": user_id, "name": "Alice"} @router.post("/") # → POST /users/ async def create_user(): return {"id": 99}
# main.py from fastapi import FastAPI from routers import users, items, orders app = FastAPI() # Include each router — optionally override their prefix/tags app.include_router(users.router) app.include_router(items.router) app.include_router( orders.router, prefix="/api/v1", # additional prefix: /api/v1/orders/... tags=["orders"], ) # Result: # GET /users/ ← from users router # GET /users/{id} ← from users router # POST /users/ ← from users router # GET /items/ ← from items router # GET /api/v1/orders/ ← from orders router (extra prefix)
Routers can include other routers — useful for versioned APIs or deeply nested resources:
# api/v1/router.py from fastapi import APIRouter from api.v1 import users, items v1_router = APIRouter(prefix="/v1") v1_router.include_router(users.router) # → /v1/users/ v1_router.include_router(items.router) # → /v1/items/ # api/v2/router.py v2_router = APIRouter(prefix="/v2") v2_router.include_router(users.router) # → /v2/users/ # main.py app = FastAPI() app.include_router(v1_router, prefix="/api") # → /api/v1/users/ app.include_router(v2_router, prefix="/api") # → /api/v2/users/
my_api/ ├── main.py ├── routers/ │ ├── users.py │ ├── items.py │ └── orders.py ├── models/ ├── services/ └── database.py
Build a route definition interactively:
Request Handling
Extract data from path parameters, query strings, headers, and cookies — with automatic type validation and conversion.
Path Parameters
Path parameters are variable parts of the URL path wrapped in {curly_braces}. FastAPI automatically extracts and validates them.
from fastapi import FastAPI, Path app = FastAPI() # Basic: type annotation = automatic validation + conversion @app.get("/users/{user_id}") async def get_user(user_id: int): # FastAPI auto-converts "42" (string) to 42 (int) # GET /users/42 → user_id = 42 ✓ # GET /users/abc → 422 Validation Error ✗ (not an int!) return {"user_id": user_id} # String path param @app.get("/files/{filename}") async def get_file(filename: str): return {"file": filename} # Multiple path params @app.get("/users/{user_id}/posts/{post_id}") async def get_post(user_id: int, post_id: int): return {"user_id": user_id, "post_id": post_id}
Use Path() to add constraints like min/max values and regex patterns:
from fastapi import FastAPI, Path from typing import Annotated app = FastAPI() @app.get("/users/{user_id}") async def get_user( user_id: Annotated[int, Path( title="User ID", # shown in docs description="The ID of the user", ge=1, # ge = greater than or equal to 1 le=999999, # le = less than or equal to example=42, # example value in docs )] ): # GET /users/0 → 422 (fails ge=1) # GET /users/42 → ✓ # GET /users/abc → 422 (not an int) return {"user_id": user_id} # Regex constraint @app.get("/items/{item_code}") async def get_item( item_code: Annotated[str, Path( pattern=r"^[A-Z]{2}-\d{4}$", # e.g. "AB-1234" example="AB-1234", )] ): # GET /items/AB-1234 → ✓ # GET /items/abc → 422 (doesn't match pattern) return {"code": item_code}
ge (≥), gt (>), le (≤), lt (<), min_length, max_length, pattern (regex).Query Parameters
Query parameters appear after the ? in the URL. Any function parameter that's not in the path is automatically treated as a query parameter.
from fastapi import FastAPI, Query from typing import Annotated, Optional app = FastAPI() # Required query param (no default) @app.get("/search") async def search(q: str): # GET /search?q=python → q="python" # GET /search → 422 (q is required!) return {"query": q} # Optional query param (has default) @app.get("/items") async def list_items( page: int = 1, limit: int = 10, active: Optional[bool] = None, # truly optional ): # GET /items → page=1, limit=10, active=None # GET /items?page=2&limit=5 → page=2, limit=5 # GET /items?active=true → active=True (auto bool!) return {"page": page, "limit": limit, "active": active} # List query param (repeated key) @app.get("/filter") async def filter_items( tags: Annotated[list[str], Query()] = [] ): # GET /filter?tags=python&tags=web&tags=api # → tags=["python", "web", "api"] return {"tags": tags} # Query with validation @app.get("/users") async def list_users( q: Annotated[Optional[str], Query( min_length=3, max_length=50, description="Search query string", )] = None ): return {"q": q}
?active=true, ?active=1, ?active=yes → all become Python True.Headers
Use Header() to read HTTP headers. FastAPI automatically converts User-Agent → user_agent (hyphen to underscore).
from fastapi import FastAPI, Header from typing import Annotated, Optional app = FastAPI() @app.get("/info") async def get_info( # "User-Agent" header → user_agent param user_agent: Annotated[Optional[str], Header()] = None, # "X-Request-ID" header → x_request_id param x_request_id: Annotated[Optional[str], Header()] = None, ): return {"user_agent": user_agent, "request_id": x_request_id} # Authorization header (most common use case) @app.get("/secure") async def secure_endpoint( authorization: Annotated[str, Header( description="Bearer token: 'Bearer your-token-here'" )] ): # Reads the "Authorization" header # Value: "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." token = authorization.replace("Bearer ", "") return {"token_preview": token[:10] + "..."} # List header (same header sent multiple times) @app.get("/multi-header") async def multi( x_token: Annotated[Optional[list[str]], Header()] = None ): return {"x_tokens": x_token}
Cookies
Use Cookie() to read cookies from incoming requests and Response to set cookies in the response.
from fastapi import FastAPI, Cookie, Response from typing import Annotated, Optional app = FastAPI() # Read a cookie @app.get("/me") async def get_current_user( session_id: Annotated[Optional[str], Cookie()] = None ): # Reads the "session_id" cookie from the request if not session_id: return {"user": "anonymous"} return {"session_id": session_id} # Set a cookie in the response @app.post("/login") async def login(response: Response): # Inject Response to set cookies response.set_cookie( key="session_id", value="abc123xyz", max_age=3600, # expire in 1 hour httponly=True, # JS can't access it secure=True, # HTTPS only samesite="lax", # CSRF protection ) return {"message": "Logged in!"} # Delete a cookie (logout) @app.post("/logout") async def logout(response: Response): response.delete_cookie("session_id") return {"message": "Logged out!"}
Click a URL to see how FastAPI parses the path, query, and header parameters:
Query() explicitly{}, it's treated as a query parameter automatically@query_param decoratorGET /users/abc when the route expects user_id: int?None422 Unprocessable EntityRequest Bodies
Handle JSON bodies, HTML forms, multipart form data, and file uploads — all with automatic validation.
JSON Bodies
Declare a Pydantic model as a parameter and FastAPI automatically reads the request body as JSON, validates it, and passes the typed object to your function.
from fastapi import FastAPI from pydantic import BaseModel from typing import Optional app = FastAPI() class CreateUserRequest(BaseModel): username: str email: str age: Optional[int] = None @app.post("/users") async def create_user(user: CreateUserRequest): # FastAPI automatically: # 1. Reads request body as JSON # 2. Validates against CreateUserRequest # 3. Returns 422 if validation fails # 4. Passes typed Python object to your function print(user.username) # "alice" ← typed! print(user.email) # "alice@example.com" print(user.age) # None or int return {"created": user.model_dump()} # Mixing path + query + body: # All 3 can coexist in one endpoint! @app.put("/users/{user_id}") async def update_user( user_id: int, # ← path param notify: bool = False, # ← query param user: CreateUserRequest = ..., # ← body ): return { "id": user_id, "notify": notify, "data": user.model_dump() }
Forms
Use Form() to receive HTML form data (application/x-www-form-urlencoded or multipart/form-data). Install python-multipart first.
pip install python-multipart
from fastapi import FastAPI, Form from typing import Annotated app = FastAPI() # URL-encoded form (typical HTML form POST) @app.post("/login") async def login( username: Annotated[str, Form()], password: Annotated[str, Form()], ): # Content-Type: application/x-www-form-urlencoded # Body: username=alice&password=secret return {"username": username, "logged_in": True} # Form with validation @app.post("/register") async def register( username: Annotated[str, Form(min_length=3, max_length=20)], email: Annotated[str, Form()], age: Annotated[int, Form(ge=18)], # must be 18+ ): return {"registered": username} # ⚠️ You cannot mix Form() and JSON body in the same endpoint # They use different Content-Types
File Uploads
Use UploadFile for file uploads. It gives you metadata (filename, content type) and async file reading.
from fastapi import FastAPI, File, UploadFile, Form from typing import Annotated app = FastAPI() # Single file upload @app.post("/upload") async def upload_file(file: UploadFile): print(file.filename) # "photo.jpg" print(file.content_type) # "image/jpeg" print(file.size) # file size in bytes # Read entire file into memory contents = await file.read() # Or stream it in chunks (better for large files!) while chunk := await file.read(1024): # 1KB chunks process_chunk(chunk) return {"filename": file.filename, "size": file.size} # Multiple files @app.post("/upload-many") async def upload_many(files: list[UploadFile]): results = [] for file in files: contents = await file.read() results.append({"name": file.filename, "size": len(contents)}) return results # File + form fields together (use multipart/form-data) @app.post("/upload-with-meta") async def upload_with_meta( file: UploadFile, description: Annotated[str, Form()], public: Annotated[bool, Form()] = False, ): return { "filename": file.filename, "description": description, "public": public, }
await file.read(chunk_size) in a loop instead of await file.read() — reading the entire file at once can exhaust server memory.For very large files, stream directly from the raw request instead of loading into memory:
from fastapi import FastAPI, Request import aiofiles app = FastAPI() @app.post("/stream-upload") async def stream_upload(request: Request): # Stream directly from request body to disk # Never loads entire file in memory! async with aiofiles.open("output.bin", "wb") as f: async for chunk in request.stream(): await f.write(chunk) return {"status": "uploaded"} # Client sends: # curl -X POST http://localhost:8000/stream-upload \ # --data-binary @huge-file.zip
@app.on_event("startup") decorator@asynccontextmanager lifespan function passed to FastAPI(lifespan=...)app.startup() methodmain.py on the app objectAPIRouter files per domain and include_router() in main.pyForm() fields and a Pydantic JSON body in the same endpoint?Body(media_type="multipart")━━━ App Creation ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ app = FastAPI(title="API", version="1.0", description="...") ━━━ Lifespan ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @asynccontextmanager async def lifespan(app): # startup yield # shutdown app = FastAPI(lifespan=lifespan) ━━━ HTTP Methods ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @app.get / post / put / patch / delete / head / options ━━━ APIRouter ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ router = APIRouter(prefix="/users", tags=["users"]) app.include_router(router) ━━━ Parameter Sources ━━━━━━━━━━━━━━━━━━━━━━━━━━ def endpoint( user_id: int, # path (in URL pattern {}) page: int = 1, # query (not in path = query) auth: Annotated[str, Header()], # header sid: Annotated[str, Cookie()], # cookie body: MyModel, # JSON body (Pydantic) form: Annotated[str, Form()], # form field file: UploadFile, # file upload ) ━━━ Constraints ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Path(ge=1, le=999, pattern=r"...") Query(min_length=3, max_length=50)