FastAPI for Spring developers: the boilerplate that isn't there
A comment I left in my own code
I’ve been poking at FastAPI in a throwaway main.py, coming at it as what I actually am: a Spring Boot engineer who has spent years wiring controllers, DTOs, mappers and validators. Sixty lines in, I noticed I’d left myself a comment above a pair of models:
# no mapstruct here, so smooth.
That throwaway line is the whole story of this post. Here is what provoked it:
class UserIn(BaseModel):
username: str
password: str
email: str
class UserOut(BaseModel):
username: str
email: str
@app.post("/users", response_model=UserOut)
def create_user(user: UserIn):
return user # the password never leaves the building
I return the full user — password and all — and the client gets back only username and email. In Spring, going from an inbound type to a different outbound type is a small ritual: an entity, a response DTO, and a MapStruct mapper with its @Mapping config and a generated impl class to shuttle the fields across. Here, response_model=UserOut is the mapper. FastAPI does not copy my object into a new one — it re-validates my return value against UserOut’s schema and serialises only the fields that shape declares. The projection is the type.
The type hint is the framework
Once I saw that, the rest of the file rhymed. In Spring, the metadata that drives a request lives in annotations: @RequestBody, @PathVariable, @RequestParam, @Valid. In FastAPI it lives in the type signature. The same jobs get done; there is just far less scaffolding standing between me and them.
![Table mapping six Spring/Java mechanisms to their FastAPI equivalents: response DTO plus MapStruct mapper becomes response_model=UserOut; @Positive plus @Valid becomes Field(gt=0); @PathVariable becomes item_id: int; @RequestParam default becomes limit: int = 10; @RequestBody plus Jackson becomes item: Item; a @Component bean becomes Annotated[dict, Depends(...)].](/blog/media/98cbd80f76c82f93716d58ca2e4f6e190baf437d68f120b708e0d4db8103a019.png)
Reading a route the way FastAPI does
Take four routes straight out of the file. Not one of them is annotated for binding; the parameter’s type is what decides its role.

Depends alias means a dependency. One rule, read off the signature.The validation story folds into the same idea. My Item model constrains a field inline:
class Item(BaseModel):
name: str
price: float = Field(gt=0)
description: str | None = None
in_stock: bool = True
Field(gt=0) is Bean Validation’s @Positive — except I do not also have to remember to hang @Valid on the argument to arm it. A body that violates the constraint returns a 422 with a precise error on its own. And description: str | None = None is an optional field with a default, no Optional<String> gymnastics to declare it.
Dependency injection, the part that made me grin
This is the one that looked most alien at a glance and turned out to be the most familiar:
def pagination_params(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
CommonPagination = Annotated[dict, Depends(pagination_params)]
@app.get("/products")
def read_items(pagination: CommonPagination):
return {"pagination": pagination, "data": ["a", "b", "c"]}
pagination_params is just a function. Wrapping it in Depends and aliasing that as CommonPagination gives me a reusable, injectable unit I drop into any route by declaring the type. And because the dependency is itself a typed function, its skip and limit become query parameters on every route that uses it — the resolution is recursive. That is my @Component plus a request-scoped argument resolver, minus the container: no @Autowired, no bean definition, no component scan. A function and a type alias.
Where it actually costs you
I am not going to pretend it is free. Spring’s annotations are checked at compile time inside a mature, boringly reliable ecosystem; FastAPI’s guarantees are runtime, enforced by Pydantic when the request lands. A wrong type is a Spring build failure and a FastAPI bug you meet in production. Refactoring across a large Java codebase leans on the compiler in a way duck-typed Python cannot match, and the JVM’s operational story — profilers, thread dumps, libraries that have been hardened for a decade — runs deeper than the Python web stack. What I buy with FastAPI is speed of writing and a signature that documents itself; what I give up is the compiler holding my hand.
And then it hands you the docs
The detail that closed the deal: because every route’s shape is declared in types, FastAPI already holds the full schema — so it generates an OpenAPI document and a live Swagger UI at /docs with no extra code. In Spring I reach for springdoc, annotate the edge cases, and keep it in sync. Here it simply exists, and it is correct by construction, because it is derived from the same signatures that serve the requests. That is the “so smooth” I had grumbled about in a code comment. The boilerplate is not hidden, and it is not generated for me behind the scenes. It just is not there.