If you come from a Spring Boot background, FastAPI will feel both familiar and refreshingly lightweight. This three-day guide turns your Spring knowledge into a practical FastAPI skillset fast. Each day has concrete goals, separate code snippets, and a short exercise so you leave with a working app.
Why this guide
- Spring Boot already taught you how to think about APIs, validation, dependency injection, persistence, and testing.
- FastAPI uses the same problem areas, but with fewer abstractions and more explicit code.
- You are not starting over. You are translating familiar backend concepts into a different framework and language.
Prerequisites
- Comfortable with Spring Boot basics: controllers, DI, JPA, and properties.
- Basic Python: functions, classes, and reading code.
- Python 3.10+ with <code>pip</code> and <code>virtualenv</code> or <code>pipx</code>/<code>poetry</code>.
Quick setup
# macOS / Linux
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn[standard] pydantic sqlalchemy alembic
# run dev server
uvicorn app.main:app --reloadDay 1 - FastAPI fundamentals
Goal: create a small FastAPI app, understand routing, dependency injection, and Pydantic models.
Spring mapping
- <code>@RestController</code> -> <code>APIRouter</code> and function endpoints
- <code>@Autowired</code> / DI -> <code>Depends()</code> and callable dependencies
- DTOs / request validation -> Pydantic models
What you should finish
- A running FastAPI app with one router and one POST endpoint.
- A clear mapping from Spring controller methods to FastAPI route functions.
- A working <code>/docs</code> page for interactive testing.
Suggested pace
- Morning: set up the virtual environment and project structure.
- Afternoon: add routes, request models, and response payloads.
- Evening: verify everything in Swagger UI and note the Spring equivalent for each piece.
1. Project scaffold
Create <code>app/main.py</code>:
from fastapi import FastAPI
from app.api import router
app = FastAPI(title="Todo (FastAPI)")
app.include_router(router)Create <code>app/api.py</code>:
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix="/api")
class TodoIn(BaseModel):
title: str
done: bool = False
@router.get("/", tags=["root"])
def root():
return {"status": "ok"}
@router.post("/todos")
def create_todo(payload: TodoIn):
return {"id": 1, **payload.model_dump()}2. Run and test
uvicorn app.main:app --reload --port 8000
# open http://localhost:8000/docs for the interactive OpenAPI UI3. Exercise
- Add <code>GET /todos/{id}</code> and <code>GET /todos</code> endpoints.
- Implement a simple in-memory store to persist todos during runtime.
Day 1 review
- Which Spring annotation did each FastAPI feature replace?
- Where did validation move in your new mental model?
- What is simpler in FastAPI than in the Spring version you know?
Day 1 checklist
- App boots with <code>uvicorn</code> and <code>/docs</code> works.
- Pydantic models handle request validation.
- One Spring controller maps to one <code>APIRouter</code>.
Day 2 - Data, persistence, and testing
Goal: replace in-memory storage with a database and write unit or integration tests.
Spring mapping
- <code>@Entity</code> + <code>JpaRepository</code> -> SQLAlchemy or SQLModel plus repository functions
- <code>@SpringBootTest</code> / Testcontainers -> <code>pytest</code>, <code>httpx</code>, and Testcontainers-Python
What you should finish
- A persistence layer that survives beyond a single request.
- At least one automated test that exercises the HTTP layer.
- A mental model for how FastAPI testing differs from <code>@SpringBootTest</code>.
Suggested pace
- Morning: choose the ORM and create the schema.
- Afternoon: wire database startup and CRUD functions.
- Evening: write tests for the happy path and one validation failure.
1. Pick an ORM approach
For Spring developers, SQLModel is a smooth, Pydantic-first ORM. Otherwise use SQLAlchemy 1.4+.
pip install sqlmodel[asyncio] databases asyncpg pytest httpx2. Minimal persistence with SQLModel and SQLite
Create <code>app/models.py</code>:
from sqlmodel import Field, SQLModel
class Todo(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
title: str
done: bool = FalseCreate <code>app/db.py</code>:
from sqlmodel import SQLModel, create_engine
engine = create_engine("sqlite:///./test.db", echo=True)
def init_db():
SQLModel.metadata.create_all(engine)Wire it in your startup:
from fastapi import FastAPI
from app.db import init_db
app = FastAPI()
@app.on_event("startup")
def on_startup():
init_db()3. Testing
Use <code>pytest</code> with <code>httpx</code> or <code>TestClient</code> for sending requests to the test server.
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_root():
response = client.get("/api/")
assert response.status_code == 2004. Exercise
- Persist todos to the DB and implement <code>POST</code>, <code>GET</code>, <code>PUT</code>, and <code>DELETE</code> for full CRUD.
- Add tests that run against in-memory SQLite, or Testcontainers if you prefer.
Day 2 review
- What is the FastAPI equivalent of a JPA repository in your app?
- Which tests feel lighter than the Spring version, and why?
- What parts of the stack still need careful design even in a smaller framework?
Day 2 checklist
- Replace the in-memory store with SQLModel or SQLAlchemy.
- Add tests with <code>pytest</code> and <code>TestClient</code>.
- Understand lifecycle hooks like <code>startup</code> and <code>shutdown</code>.
Day 3 - Security, async, deployment, and Spring analogies
Goal: add auth, understand async patterns, containerize, and deploy a small app.
1. Security basics
- Spring: <code>spring-boot-starter-security</code> and modern security configuration.
- FastAPI: OAuth2 + JWT or packages like <code>fastapi-users</code>.
What this teaches you:
- Spring security is powerful but layered.
- FastAPI security is usually smaller and more direct.
- The important part is still the same: protect routes, validate identity, and keep auth logic out of business handlers.
Minimal token flow:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
def get_current_user(token: str = Depends(oauth2_scheme)):
if token != "fake-token":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return {"username": "dev"}2. Async concurrency
- FastAPI supports <code>async def</code> endpoints.
- Prefer async DB drivers like <code>asyncpg</code> when using async endpoints.
- Spring is often thought of as thread per request; FastAPI pushes you toward the event loop and non-blocking work.
This matters because Spring developers often think in terms of request threads, interceptors, and servlet filters. FastAPI pushes you to think in terms of awaitable work, event loops, and blocking calls that should be isolated.
3. Containerize and deploy
Dockerfile example:
FROM python:3.11-slim
WORKDIR /app
COPY pyproject.toml requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]Build and run:
docker build -t todo-fastapi:latest .
docker run -p 8000:80 todo-fastapi:latest4. Exercise
- Add JWT auth to protect the <code>POST</code>, <code>PUT</code>, and <code>DELETE</code> endpoints.
- Containerize the app and deploy it to your cloud of choice.
Day 3 review
- Which parts of your Spring Boot experience transfer directly?
- Which parts need a new mental model in FastAPI?
- What would you keep the same if you were rebuilding this service in Python?
Day 3 checklist
- Protect endpoints with OAuth2 or JWT.
- Understand async endpoints and when to use them.
- Build the Docker image and run it locally.
Common Spring to FastAPI mappings
- Controllers: <code>@RestController</code> -> <code>APIRouter</code> and function endpoints
- DI: <code>@Autowired</code>, <code>@Service</code> -> <code>Depends()</code> and explicit dependency callables
- DTOs and validation: Javax or Bean Validation -> Pydantic models and validators
- Config: <code>application.properties</code> -> Pydantic <code>BaseSettings</code> or environment variables
- Lifecycle: <code>@PostConstruct</code> and <code>@PreDestroy</code> -> <code>@app.on_event("startup")</code> and shutdown hooks
- Testing: <code>@SpringBootTest</code> -> <code>pytest</code> plus <code>TestClient</code> or Testcontainers
Further resources
- Official FastAPI docs: <a href="https://fastapi.tiangolo.com" target="_blank" rel="noreferrer">fastapi.tiangolo.com</a>
- SQLModel docs: <a href="https://sqlmodel.tiangolo.com" target="_blank" rel="noreferrer">sqlmodel.tiangolo.com</a>
- FastAPI Tutorials and Examples: <a href="https://github.com/tiangolo/fastapi" target="_blank" rel="noreferrer">github.com/tiangolo/fastapi</a>
- Async Python: <a href="https://docs.python.org/3/library/asyncio.html" target="_blank" rel="noreferrer">docs.python.org/3/library/asyncio.html</a>
Final tips
- Start with synchronous code if you do not need concurrency; convert endpoints to <code>async def</code> once the DB and libraries support it.
- Use Pydantic models aggressively; they replace a lot of Java boilerplate.
- Embrace the OpenAPI docs at <code>/docs</code> because they are useful for iterating quickly.
- If you rely on Spring ecosystem features such as security, data, or cloud, look for mature FastAPI libraries before re-implementing from scratch.
- If you already know Spring Boot, treat FastAPI as a translation exercise: controllers become routes, DTOs become Pydantic models, services stay services, and tests become more direct.
Want a starter repo scaffold with FastAPI, SQLModel, tests, a Dockerfile, and JWT auth? I can generate it next.
Deep dive - recipes and examples
Below are ready-to-copy recipes to accelerate each day. They are intentionally small, focused, and annotated so you can paste them into your project and run them quickly.
In-memory store (complete example)
Create <code>app/store.py</code> for a tiny runtime-backed store used during Day 1 exercises:
# app/store.py
from typing import List, Optional
from threading import Lock
_lock = Lock()
_todos: List[dict] = []
_next_id = 1
def list_todos() -> List[dict]:
return list(_todos)
def get_todo(todo_id: int) -> Optional[dict]:
return next((t for t in _todos if t["id"] == todo_id), None)
def create_todo(data: dict) -> dict:
global _next_id
with _lock:
item = {"id": _next_id, **data}
_next_id += 1
_todos.append(item)
return item
def update_todo(todo_id: int, data: dict) -> Optional[dict]:
with _lock:
t = get_todo(todo_id)
if not t:
return None
t.update(data)
return t
def delete_todo(todo_id: int) -> bool:
with _lock:
global _todos
new = [t for t in _todos if t["id"] != todo_id]
if len(new) == len(_todos):
return False
_todos = new
return TrueWire into <code>app/api.py</code> with <code>Depends</code> to show a simple DI pattern:
from fastapi import Depends
def get_store():
# could swap for a DB-backed store later
from app.store import list_todos, create_todo, get_todo
return {"list": list_todos, "create": create_todo, "get": get_todo}
@router.get("/todos")
def list_all(store=Depends(get_store)):
return store["list"]()
@router.get("/todos/{id}")
def read(id: int, store=Depends(get_store)):
t = store["get"](id)
if not t:
raise HTTPException(status_code=404)
return t
@router.post("/todos")
def create(payload: TodoIn, store=Depends(get_store)):
return store["create"](payload.model_dump())SQLModel CRUD pattern (sync)
Create <code>app/crud.py</code> to isolate DB access (keep your routers thin):
# app/crud.py
from sqlmodel import Session, select
from app.db import engine
from app.models import Todo
def create_todo(session: Session, title: str, done: bool = False) -> Todo:
todo = Todo(title=title, done=done)
session.add(todo)
session.commit()
session.refresh(todo)
return todo
def get_todo(session: Session, todo_id: int) -> Todo | None:
return session.get(Todo, todo_id)
def list_todos(session: Session):
return session.exec(select(Todo)).all()
def update_todo(session: Session, todo_id: int, **fields):
t = session.get(Todo, todo_id)
if not t:
return None
for k, v in fields.items():
setattr(t, k, v)
session.add(t)
session.commit()
session.refresh(t)
return t
def delete_todo(session: Session, todo_id: int) -> bool:
t = session.get(Todo, todo_id)
if not t:
return False
session.delete(t)
session.commit()
return TrueIn your route, open a session per-request:
from sqlmodel import Session
from app.db import engine
from app.crud import create_todo, list_todos
@router.post('/todos')
def create_api(payload: TodoIn):
with Session(engine) as session:
return create_todo(session, **payload.model_dump())
@router.get('/todos')
def list_api():
with Session(engine) as session:
return list_todos(session)Alembic quick start
- <code>pip install alembic</code>
- <code>alembic init alembic</code>
- Edit <code>alembic/env.py</code> to import <code>SQLModel.metadata</code> and set the <code>target_metadata</code>.
- <code>alembic revision --autogenerate -m "create todos"</code>
- <code>alembic upgrade head</code>
Include a short note: prefer running migrations from your dev/startup script, and keep <code>alembic</code> out of normal request paths.
Tests - sync and async examples
Sync <code>pytest</code> using <code>TestClient</code>:
# tests/test_api_sync.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_root():
r = client.get('/api/')
assert r.status_code == 200Async <code>pytest</code> using <code>httpx.AsyncClient</code> and an <code>async</code> test server (useful for async DB drivers):
# tests/test_api_async.py
import pytest
from httpx import AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_root_async():
async with AsyncClient(app=app, base_url='http://test') as ac:
r = await ac.get('/api/')
assert r.status_code == 200Database fixtures (sqlite in-memory) — example:
# tests/conftest.py
import pytest
from sqlmodel import SQLModel, create_engine, Session
from app.main import app
@pytest.fixture(scope='function')
def db_engine():
engine = create_engine('sqlite:///:memory:')
SQLModel.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture
def client(db_engine):
# monkeypatch app.db.engine or inject session factory
yield from []Notes: adapt the fixtures to your chosen DB driver (sync vs async) and prefer <code>pytest-asyncio</code> for async tests.
JWT auth recipe (simple)
Dependencies: <code>pip install python-jose[cryptography] passlib[bcrypt]</code>
# app/auth.py
from datetime import datetime, timedelta
from jose import jwt
from passlib.context import CryptContext
SECRET = 'change-me-in-production'
ALGO = 'HS256'
pwd_ctx = CryptContext(schemes=['bcrypt'], deprecated='auto')
def hash_password(password: str) -> str:
return pwd_ctx.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_ctx.verify(plain, hashed)
def create_access_token(sub: str, minutes: int = 15) -> str:
now = datetime.utcnow()
payload = {'sub': sub, 'iat': now, 'exp': now + timedelta(minutes=minutes)}
return jwt.encode(payload, SECRET, algorithm=ALGO)Protect endpoints with <code>OAuth2PasswordBearer</code> and verify the token in a dependency (see Day 3 example above). For production, rotate secrets and use asymmetric keys if possible.
Docker - multi-stage build and runtime notes
Use a multi-stage Dockerfile to install build dependencies separately and keep final image small:
FROM python:3.11-slim as builder
WORKDIR /app
COPY pyproject.toml requirements.txt ./
RUN pip install --prefix=/install -r requirements.txt
FROM python:3.11-slim
COPY --from=builder /install /usr/local
WORKDIR /app
COPY . .
ENV PYTHONUNBUFFERED=1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]Runtime tuning tips:
- Use <code>--workers</code> via <code>gunicorn</code> + <code>uvicorn.workers.UvicornWorker</code> for multi-core.
- Set timeout and keep-alive to match your cloud provider defaults.
CI example - GitHub Actions (minimal)
Create <code>.github/workflows/ci.yml</code>:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: python -m pip install -r requirements.txt
- run: pytest -q--- If you'd like, I can now generate a starter scaffold repo with these files (minimal FastAPI app, <code>app/store.py</code>, <code>app/models.py</code>, <code>app/db.py</code>, <code>app/crud.py</code>, tests, Dockerfile, and CI). Tell me which pieces you want generated and I'll create them next.