Jump to content

FastAPI vs Flask in 2025

From JOHNWICK
Revision as of 22:26, 13 December 2025 by PC (talk | contribs) (Created page with "650px FastAPI vs Flask in 2025: a practical, async-first guide with code, trade-offs, pitfalls, and a 10-minute blueprint to ship your indie SaaS backend. You have a weekend, a coffee, and a SaaS idea. The question isn’t “what stack?” — it’s “what ships fastest without painting me into a corner?” Let’s be real: in 2025, that often means choosing between FastAPI and Flask. Why this comparison still matters Fl...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

FastAPI vs Flask in 2025: a practical, async-first guide with code, trade-offs, pitfalls, and a 10-minute blueprint to ship your indie SaaS backend.


You have a weekend, a coffee, and a SaaS idea. The question isn’t “what stack?” — it’s “what ships fastest without painting me into a corner?” Let’s be real: in 2025, that often means choosing between FastAPI and Flask.


Why this comparison still matters Flask is the seasoned minimalist. FastAPI is the async-native sprinter with batteries included. Both are excellent — but they push you toward different development rhythms and deployment shapes. If you’re building a small-but-serious indie SaaS, the defaults you pick today will echo in every feature, cron, and incident you handle later.


TL;DR: Which should you choose?

  • Pick FastAPI if you want async I/O, built-in Pydantic validation, OpenAPI docs out of the box, and you expect real-time features or external API fan-out (think Stripe + Slack + Notion in one request).
  • Pick Flask if you value utter simplicity, huge ecosystem familiarity, and you’re okay adding pieces yourself — or you’re migrating an older WSGI stack.


Architecture at a glance (10-minute Indie SaaS)

[Browser/CLI]
     |
     v
+-------------+        Webhooks      +-----------+
|  API (ASGI) |  <-----------------  | Providers |
|  FastAPI    |  --> Task Queue ---> | Stripe    |
|  Uvicorn    |  <-- Cache (Redis)   | GitHub    |
+-------------+                      +-----------+
      |            ^
      v            |
  Postgres <-------+

Why this works: ASGI + async DB drivers let you overlap I/O: external APIs, database, cache, and webhooks. You gain latency headroom without horizontal sprawl.


The 10-minute FastAPI backend (real, minimal, async) 1) Project layout

saas/
  app.py
  models.py
  schema.py
  requirements.txt
requirements.txt
fastapi
uvicorn[standard]
pydantic
sqlmodel
asyncpg
sqlalchemy>=2.0
python-jose[cryptography]
passlib[bcrypt]

2) Models and schema (SQLModel + Pydantic)

# models.py
from sqlmodel import SQLModel, Field
from typing import Optional
import datetime as dt

class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    email: str = Field(index=True, unique=True)
    password_hash: str
    created_at: dt.datetime = Field(default_factory=dt.datetime.utcnow)

class Todo(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    owner_id: int = Field(index=True)
    title: str
    done: bool = False
    created_at: dt.datetime = Field(default_factory=dt.datetime.utcnow)

# schema.py
from pydantic import BaseModel, EmailStr

class SignupIn(BaseModel):
    email: EmailStr
    password: str

class LoginIn(BaseModel):
    email: EmailStr
    password: str

class TodoIn(BaseModel):
    title: str

class TodoOut(BaseModel):
    id: int
    title: str
    done: bool

3) App with auth + CRUD (JWT + async DB)

# app.py
import asyncio
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlmodel import SQLModel, select
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from jose import jwt, JWTError
from passlib.hash import bcrypt
from models import User, Todo
from schema import SignupIn, LoginIn, TodoIn, TodoOut

SECRET = "change-me"
ALGO = "HS256"

engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/saas", future=True, echo=False)
Session = async_sessionmaker(engine, expire_on_commit=False)
auth_scheme = HTTPBearer()

app = FastAPI(title="Indie SaaS API", version="2025.1")

@app.on_event("startup")
async def on_startup():
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)

def create_token(user_id: int) -> str:
    return jwt.encode({"sub": str(user_id)}, SECRET, algorithm=ALGO)

async def get_user(creds: HTTPAuthorizationCredentials = Depends(auth_scheme)):
    try:
        payload = jwt.decode(creds.credentials, SECRET, algorithms=[ALGO])
        uid = int(payload["sub"])
    except (JWTError, KeyError, ValueError):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    async with Session() as s:
        user = await s.get(User, uid)
        if not user:
            raise HTTPException(status_code=401)
        return user

@app.get("/health")
async def health():
    return {"ok": True}

@app.post("/signup")
async def signup(data: SignupIn):
    async with Session() as s:
        exists = (await s.exec(select(User).where(User.email == data.email))).first()
        if exists:
            raise HTTPException(409, "Email already used")
        user = User(email=data.email, password_hash=bcrypt.hash(data.password))
        s.add(user)
        await s.commit()
        await s.refresh(user)
        return {"token": create_token(user.id)}

@app.post("/login")
async def login(data: LoginIn):
    async with Session() as s:
        user = (await s.exec(select(User).where(User.email == data.email))).first()
        if not user or not bcrypt.verify(data.password, user.password_hash):
            raise HTTPException(401, "Invalid credentials")
        return {"token": create_token(user.id)}

@app.post("/todos", response_model=TodoOut)
async def create_todo(inp: TodoIn, user=Depends(get_user)):
    async with Session() as s:
        todo = Todo(owner_id=user.id, title=inp.title)
        s.add(todo)
        await s.commit()
        await s.refresh(todo)
        return TodoOut(id=todo.id, title=todo.title, done=todo.done)

@app.get("/todos", response_model=list[TodoOut])
async def list_todos(user=Depends(get_user)):
    async with Session() as s:
        rows = await s.exec(select(Todo).where(Todo.owner_id == user.id))
        return [TodoOut(id=t.id, title=t.title, done=t.done) for t in rows]
Run it locally
uvicorn app:app --reload

Auto docs at /docs and /redoc—zero extra config. That’s minutes saved.


“Can Flask do async in 2025?” Short answer: yes, with caveats. Since Flask 2+, you can write async def routes, but Flask is still WSGI-first. You’ll need the right server or adapters to get true ASGI-style concurrency. If you want native ASGI and typed request/response models, FastAPI feels smoother. If your app is mostly synchronous templates + a few APIs, Flask remains a joy.

Minimal Flask 3 route

from flask import Flask, jsonify
app = Flask(__name__)

@app.get("/health")
async def health():
    return jsonify(ok=True)

Great DX, huge ecosystem — but you’ll assemble validation, schema, and docs yourself (or via extensions).


Real-world numbers (indicative, not gospel) On a base M2 laptop with a simple JSON endpoint:

  • FastAPI + Uvicorn (ASGI): ~5,000–6,500 req/s with wrk -t4 -c128 -d30s.
  • Flask (WSGI dev server): ~1,000–1,800 req/s; with a tuned production server and caching you can go higher.

Your mileage will vary — DB I/O, auth, and network hops dominate real apps. But async buys you headroom when calling 2–3 external APIs per request.


Developer ergonomics that compound

  • Validation + types: FastAPI’s Pydantic models catch bad payloads early and generate docs. That can cut 20–30% off integration bugs in early builds.
  • Docs for free: Clients can try endpoints live in /docs. Support tickets mysteriously drop.
  • Async “fan-out” patterns: Call Stripe, Slack, and your DB concurrently with asyncio.gather. Latency feels smaller than the sum of its parts.


When Flask is still the right move

  • You’re migrating a legacy WSGI app or rely on battle-tested Flask extensions.
  • Your app is mostly server-rendered pages; API is small and simple.
  • You want the thinnest layer possible and prefer to hand-pick everything: Marshmallow, Flasgger, SQLAlchemy classic, Jinja2, Celery, etc.


Comparison table Criterion FastAPI (ASGI) Flask (WSGI-first) Async support Native; first-class Supported in routes, but WSGI core; ASGI via adapters Validation & typing Pydantic models by default Optional via extensions Auto OpenAPI docs Built-in (/docs) Extension-based Performance under I/O Strong (concurrency) Good, often needs more processes Learning curve Moderate Very low Ecosystem age Newer but vibrant Mature, massive Best fit API-first, async SaaS Minimal sites, legacy, full control


Pitfalls to avoid

  • Blocking the event loop: Don’t do CPU-heavy work in FastAPI routes. Offload to a worker (Celery/RQ) or run in a process pool.
  • Mixing sync DB drivers in async apps: Use asyncpg/SQLAlchemy 2.0 async.
  • JWT everywhere: For server-side sessions and revocation, consider fast session stores over pure JWT.
  • Ignoring timeouts and retries: Wrap external calls with sensible timeouts; add circuit breakers.
  • Staging ≠ prod: Measure under realistic load. Tiny features can cascade into big latencies when calling 3rd parties.


Mini case study: “Ship in a weekend, survive for a year” An indie founder launched a subscription notes API with FastAPI, Postgres, and Stripe webhooks. They started with two endpoints and a /health route, then added Slack notifications and a Notion sync. Because the code was async from day one, adding parallel calls cut response time from ~480 ms to ~190 ms under load. Costs stayed low—one small VM, one managed Postgres, and Redis for rate limits.


The 10-minute checklist (print this)

  • FastAPI + Uvicorn template running
  • Postgres + async SQLAlchemy connected
  • Auth (JWT or server sessions)
  • /health + /metrics (Prometheus)
  • Request timeouts + retries
  • CORS + rate limiting (e.g., middleware + Redis)
  • Background tasks (Celery/RQ) for slow jobs
  • Infrastructure as code (a single Terraform file beats none)


Conclusion (and what to do next) Both frameworks can take you to production. FastAPI wins when you want async throughput, typed contracts, and autopilot docs. Flask wins when you crave minimalism and the comfort of a decade-deep ecosystem.

If your indie SaaS leans API-first and integration-heavy, start FastAPI, keep it boring (Postgres, Redis), and iterate. If you’re migrating or templating-heavy, Flask is still a delight. Your move: Which one are you using this weekend — and why?

Read the full article here: https://medium.com/@komalbaparmar007/fastapi-vs-flask-in-2025-83ba0a5e9aba