pythonfastapiapi

Back to Python with FastAPI

4 min read

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 uv
  • uvicorn → ASGI server
  • fastapi_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.