FastAPI + SQLModel: The Python API Stack I Actually Recommend in 2026
I’ve built APIs with Flask, FastAPI, Django REST, aiohttp, and a few I’ve tried to forget. In 2026, FastAPI + SQLModel is my default stack. Here’s exactly why.
Why Not Just Django?
Django is great for content sites and when you need the full batteries-included framework. But for APIs?
- ORM is great until it isn’t — complex queries get ugly fast
- Class-based views feel dated when you’ve seen Starlette’s elegance
- The “Django way” fights you if you want modern patterns
The Stack
FastAPI → async HTTP layer
SQLModel → ORM + Pydantic (unified)
PostgreSQL → database
Alembic → migrations
One type system. One validation layer. From request to database and back.
The Code: A Real Endpoint
from sqlmodel import SQLModel, Field, Relationship
from typing import Optional, List
from datetime import datetime
import fastapi
from fastapi.middleware.cors import CORSMiddleware
# ---- Models are Pydantic + ORM in one ----
class UserBase(SQLModel):
email: str = Field(unique=True, index=True)
username: str
class User(UserBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
posts: List["Post"] = Relationship(back_populates="author")
created_at: datetime = Field(default_factory=datetime.utcnow)
class Post(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str
content: str
author_id: int = Field(foreign_key="user.id")
author: Optional[User] = Relationship(back_populates="posts")
# ---- Request/Response models (separate from DB) ----
class PostCreate(SQLModel):
title: str = Field(min_length=1, max_length=200)
content: str
class PostRead(PostCreate):
id: int
author_id: int
# ---- API ----
app = FastAPI(title="CodeClash API")
@app.post("/posts", response_model=PostRead, status_code=201)
def create_post(post: PostCreate, db: Session = Depends(get_db)):
db_post = Post.model_validate(post)
db.add(db_post)
db.commit()
db.refresh(db_post)
return db_post
@app.get("/posts", response_model=List[PostRead])
def list_posts(db: Session = Depends(get_db)):
return db.exec(select(Post)).all()
What Makes This Work
1. SQLModel = Pydantic + SQLAlchemy
No more two separate model classes. Post is the database model AND the response model, with clean separation via inheritance.
class Post(SQLModel, table=True): # → database table
class PostRead(SQLModel): # → API response
class PostCreate(SQLModel): # → request validation
2. Auto-generated docs that actually help
FastAPI auto-generates OpenAPI docs. With proper Pydantic models, your docs are typed. Frontend devs love this.
3. Async-first, sync-when-needed
# Async endpoint — for I/O heavy operations
@app.get("/posts/{id}")
async def get_post(id: int):
return await db.fetch_one("SELECT * FROM posts WHERE id = $1", id)
# Or use SQLAlchemy's async session
from sqlalchemy.ext.asyncio import create_async_engine
The Migration Story
Alembic handles schema changes cleanly:
alembic revision --autogenerate -m "add posts table"
alembic upgrade head
What I’d Add for Production
| Tool | Purpose |
|---|---|
| Redis | Caching + rate limiting |
| Celery or RQ | Background jobs |
| Pydantic Settings | Environment config |
| Sentry | Error tracking |
| pytest + httpx | Async testing |
The stack is lean, readable, and actually fun to work with. FastAPI’s auto-generated OpenAPI docs combined with SQLModel’s unified type layer means less boilerplate and fewer “typo causing runtime errors” situations.
Try it on a side project. You’ll switch.
Enjoying the content? Here are tools I personally use and recommend:
- 🌐 Hosting: Bluehost — what this blog runs on
- 🛒 Tech Gear: My Amazon Store — keyboards, monitors, dev tools I use
Purchases through my links help keep this blog ad-free 💙
Enjoyed this post?
Subscribe to the newsletter or follow on YouTube for more dev content.
🎬 Watch Shorts