Build a Production-Ready FastAPI Backend in 2026: 5 Templates That Ship in Minutes
Every backend developer has spent hours rebuilding the same foundation: auth, database setup, error handling, logging, Docker config. In 2026, with AI tooling everywhere, there's no excuse for starting from zero.
FastAPI has become the de-facto Python backend framework. It's fast, type-safe, auto-documents itself, and deploys beautifully on any cloud. Here are 5 production-ready templates that will save you days of setup.
Template 1: Minimal FastAPI + Security Baseline
The foundation. Everything you need, nothing you don't.
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
import logging
app = FastAPI(title="My API", version="1.0.0", docs_url=None) # disable docs in prod
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["yourdomain.com", "*.yourdomain.com"])
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info(f"{request.method} {request.url.path}")
response = await call_next(request)
return response
@app.get("/health")
async def health():
return {"status": "ok"}
Why it matters: most tutorials skip CORS hardening, trusted hosts, and request logging. In production, these save you from silent failures and security incidents.
Template 2: JWT Auth with Refresh Token Rotation
The gold standard for stateless auth in 2026.
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from datetime import datetime, timedelta, timezone
import jwt
import secrets
SECRET_KEY = "your-256-bit-secret"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE = 15 # minutes
REFRESH_TOKEN_EXPIRE = 30 # days
security = HTTPBearer()
def create_access_token(user_id: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE)
return jwt.encode({"sub": user_id, "exp": expire, "type": "access"}, SECRET_KEY, ALGORITHM)
def create_refresh_token() -> str:
return secrets.token_urlsafe(64)
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("type") != "access":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return payload["sub"]
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
Key decision in 2026: refresh tokens should be rotated on every use (one-time use). This means if an attacker steals a refresh token, they get exactly one use before the token is invalidated — and you'll see the anomaly immediately.
Template 3: CRUD with Pagination + SQLModel
SQLModel bridges SQLAlchemy and Pydantic perfectly.
from fastapi import FastAPI, Depends, Query
from sqlmodel import Field, SQLModel, Session, create_engine, select
from typing import Optional
class Task(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str = Field(max_length=200)
completed: bool = False
created_at: str = Field(default="")
engine = create_engine("sqlite:///./tasks.db")
def get_session():
with Session(engine) as session:
yield session
@app.get("/tasks")
async def list_tasks(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
session: Session = Depends(get_session)
):
offset = (page - 1) * size
tasks = session.exec(select(Task).offset(offset).limit(size)).all()
total = session.exec(select(Task)).count if hasattr(select(Task), 'count') else len(session.exec(select(Task)).all())
return {
"data": tasks,
"page": page,
"size": size,
"total": total,
"pages": (total + size - 1) // size
}
Always paginate. An API that returns "all records" is a production incident waiting to happen.
Template 4: Docker Multi-Stage Build
Your FastAPI app in the smallest possible production image.
# Stage 1: Build dependencies
FROM python:3.12-slim as builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY requirements.txt .
RUN uv pip install --system --no-cache -r requirements.txt
# Stage 2: Production image
FROM python:3.12-slim as production
WORKDIR /app
# Security: non-root user
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --gid 1001 appuser
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
With uv (2024-2026's pip replacement), dependency installation is 10-100x faster. Multi-stage keeps your final image under 200MB. Non-root user is non-negotiable in 2026.
Template 5: Background Tasks + Redis Queue
For anything that shouldn't block the HTTP response.
from fastapi import FastAPI, BackgroundTasks
from redis import Redis
import json
import uuid
redis = Redis(host="localhost", port=6379, decode_responses=True)
def process_export(task_id: str, user_id: str, filters: dict):
"""Runs in background — generate CSV, send email, etc."""
redis.set(f"task:{task_id}:status", "processing", ex=3600)
try:
# ... your heavy processing here
result_url = f"https://cdn.example.com/exports/{task_id}.csv"
redis.set(f"task:{task_id}:result", result_url, ex=3600)
redis.set(f"task:{task_id}:status", "done", ex=3600)
except Exception as e:
redis.set(f"task:{task_id}:status", f"error:{str(e)}", ex=3600)
@app.post("/exports")
async def create_export(
filters: dict,
background_tasks: BackgroundTasks,
user_id: str = Depends(get_current_user)
):
task_id = str(uuid.uuid4())
redis.set(f"task:{task_id}:status", "queued", ex=3600)
background_tasks.add_task(process_export, task_id, user_id, filters)
return {"task_id": task_id, "status": "queued"}
@app.get("/exports/{task_id}/status")
async def get_export_status(task_id: str):
status = redis.get(f"task:{task_id}:status")
result = redis.get(f"task:{task_id}:result")
return {"status": status or "not_found", "result": result}
The pattern: return immediately with a task_id, poll for status. Your users get instant feedback, your server handles load gracefully.
The Full Stack: What These Templates Give You Together
| Template | Covers |
|---|---|
| Minimal + Security | CORS, logging, health check |
| JWT Auth | Stateless auth, refresh rotation |
| CRUD + Pagination | Database, filtering, safe listing |
| Docker Multi-Stage | Production deployment, security |
| Background Tasks | Async processing, user feedback |
Build them together and you have a production-ready SaaS backend — the kind that handles 10,000 users without breaking a sweat.
Going Further
Want all 5 templates in a single ready-to-use pack, with full project structure, environment configuration, testing setup, and deployment guides for Railway, Fly.io, and AWS?
I've packaged everything into the Python FastAPI Production Starter Kit — 5 complete templates, pre-wired and ready to deploy. Stop rebuilding boilerplate, start shipping features.
What FastAPI pattern do you always include in your projects? Drop it in the comments.
Top comments (0)