FastAPI + SQLModel: The Python API Stack I Actually Recommend in 2026

📅 April 10, 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

ToolPurpose
RedisCaching + rate limiting
Celery or RQBackground jobs
Pydantic SettingsEnvironment config
SentryError tracking
pytest + httpxAsync 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