Back to Python with FastAPI
I spent a good amount of time with Python during my college years, exploring scripting, small web projects, and mainly for competitive programming, even though I didn't have it in my course work despite being a CS undergrad. Over time though, I found my footing in Java, where Spring Boot became my go-to framework for backend development.
Recently, I started exploring FastAPI. What caught my attention was:
- Explicit, Pythonic code with type hints
- Request/response validation built-in (Pydantic)
- Async support out of the box
- Interactive API docs (Swagger and ReDoc) without configuration
In this tutorial, I'll build a small CRUD API with FastAPI and SQLAlchemy, while comparing each piece with Spring Boot. We'll use Astral's uv
(thanks to my special person for introducing this) to set up dependencies in a clean and reproducible way.
1. Project Setup with uv
First, install uv
(if not already):
curl -LsSf https://astral.sh/uv/install.sh | sh
Now create a new project:
uv init fastapi-todo
cd fastapi-todo
This creates a structured Python project with a pyproject.toml
file.
uv add fastapi uvicorn sqlalchemy psycopg2
In Spring Boot, you'd initialize a project with Spring Initializr, then add dependencies in pom.xml
or build.gradle
. With uv
, dependency management felt just as clean, and reproducible environments are built-in, similar to Maven's dependency locking.
2. Define the Database Model
FastAPI + SQLAlchemy model:
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
completed = Column(Boolean, default=False)
Spring Boot Comparison:
@Entity
@Table(name = "todos")
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private boolean completed;
}
3. Database Connection
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from .models import Base
DATABASE_URL = "postgresql://user:password@localhost/tododb"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
In Spring Boot, this is hidden inside application.properties
:
spring.datasource.url=jdbc:postgresql://localhost:5432/tododb
spring.datasource.username=user
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
4. Pydantic Schemas
from pydantic import BaseModel
class TodoBase(BaseModel):
title: str
completed: bool = False
class TodoCreate(TodoBase):
pass
class TodoResponse(TodoBase):
id: int
class Config:
orm_mode = True
This is equivalent to writing DTO classes in Java:
public class TodoDto {
private Long id;
private String title;
private boolean completed;
}
5. CRUD Operations
from sqlalchemy.orm import Session
from . import models, schemas
def get_todos(db: Session):
return db.query(models.Todo).all()
def get_todo(db: Session, todo_id: int):
return db.query(models.Todo).filter(models.Todo.id == todo_id).first()
def create_todo(db: Session, todo: schemas.TodoCreate):
db_todo = models.Todo(title=todo.title, completed=todo.completed)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
Spring Boot Comparison:
public interface TodoRepository extends JpaRepository<Todo, Long> {}
6. API Endpoints
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from . import database, schemas, crud
app = FastAPI()
def get_db():
db = database.SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/todos/", response_model=schemas.TodoResponse)
def create(todo: schemas.TodoCreate, db: Session = Depends(get_db)):
return crud.create_todo(db, todo)
@app.get("/todos/", response_model=list[schemas.TodoResponse])
def read_all(db: Session = Depends(get_db)):
return crud.get_todos(db)
Spring Boot Comparison:
@RestController
@RequestMapping("/todos")
public class TodoController {
@Autowired
private TodoRepository repo;
@PostMapping
public Todo create(@RequestBody Todo todo) {
return repo.save(todo);
}
@GetMapping
public List<Todo> findAll() {
return repo.findAll();
}
}
7. Running with Uvicorn (via uv)
Unlike Spring Boot (which ships with Tomcat embedded), FastAPI needs an ASGI server. The most common is Uvicorn.
Run your app with:
uv run uvicorn fastapi_todo.main:app --reload
Explanation:
uv run
→ runs in the virtual environment created by uvuvicorn
→ ASGI serverfastapi_todo.main:app
→ path to the FastAPI instance--reload
→ auto-reload on code changes (development mode)
In Java, you'd run:
mvn spring-boot:run
Final Thoughts
After building this small API, I have to say... it kinda rocks!
The developer experience with FastAPI is genuinely impressive. The automatic API documentation, built-in validation, and the way type hints integrate with everything feels like magic compared to the boilerplate I'm used to in Spring Boot.
While Spring Boot has its strengths (mature ecosystem, enterprise features, excellent tooling), FastAPI's simplicity and modern Python features make it a compelling choice for new projects. The uv
tooling makes dependency management as smooth as Maven, and the async-first approach feels more natural for modern web APIs.
Would I switch completely? Maybe not for large enterprise applications, but for new projects and microservices? FastAPI is definitely worth considering.