Build a REST API with FastAPI
FastAPI is the Python web framework closest to Spring Web in shape: typed handlers, declarative request and response models, dependency injection, and OpenAPI docs generated from the code. This page maps a Spring @RestController onto a FastAPI router.
Install it with the standard extras, which bundle the server and the CLI:
uv add "fastapi[standard]"
Controllers become routers
A Spring @RestController with @RequestMapping("/users") maps onto a FastAPI APIRouter. The HTTP-method annotations map onto route decorators:
# src/my_service/users/router.py
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/{user_id}")
def get_user(user_id: int):
return {"id": user_id}
| Spring Web | FastAPI |
|---|---|
@RestController |
an APIRouter |
@RequestMapping("/users") |
APIRouter(prefix="/users") |
@GetMapping, @PostMapping |
@router.get, @router.post |
@PathVariable Long id |
a typed path parameter user_id: int |
@RequestParam |
a typed parameter with a default |
@RequestBody Dto |
a pydantic model parameter |
{user_id} in the path and the user_id: int parameter line up by name. FastAPI parses and coerces the value, returning a 422 when the path segment is not an integer, the way Spring rejects a malformed @PathVariable.
Request and response models are pydantic models
Spring binds a JSON body to a DTO and serializes a return value through Jackson. FastAPI uses pydantic models for both, and response_model fixes the output shape:
from pydantic import BaseModel, ConfigDict, EmailStr
class CreateUser(BaseModel):
email: EmailStr
display_name: str
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True) # read fields off an ORM object
id: int
email: EmailStr
display_name: str
@router.post("", response_model=UserOut, status_code=201)
def create_user(body: CreateUser):
saved = save(body) # an object with id, email, display_name
return saved
body: CreateUser is your @RequestBody plus @Valid in one. FastAPI validates the incoming JSON against the model and returns a 422 with the field errors when it fails. response_model=UserOut is the contract for what leaves, so internal fields never reach the response.
Query parameters carry validation too
A parameter with a default becomes a query parameter. Add constraints with Query:
from typing import Annotated
from fastapi import Query
@router.get("")
def list_users(limit: Annotated[int, Query(ge=1, le=100)] = 20):
return fetch_users(limit)
This is @RequestParam(defaultValue = "20") with @Max(100) folded in.
Inject services with Depends
The provider from page 4 plugs into a route. Declare the dependency as a typed parameter:
from typing import Annotated
from fastapi import Depends
def get_user_service() -> UserService:
return UserService()
@router.get("/{user_id}", response_model=UserOut)
def get_user(user_id: int, service: Annotated[UserService, Depends(get_user_service)]):
return service.get(user_id)
Depends(get_user_service) is @Autowired, scoped to the request. FastAPI resolves the provider before the handler runs.
Map errors with exception handlers
Spring translates exceptions to responses with ResponseStatusException and @ControllerAdvice. FastAPI uses HTTPException for direct cases and a registered handler for app-wide mapping of your domain errors:
from fastapi import HTTPException
@router.get("/{user_id}", response_model=UserOut)
def get_user(user_id: int, service: Annotated[UserService, Depends(get_user_service)]):
user = service.get(user_id)
if user is None:
raise HTTPException(status_code=404, detail="user not found")
return user
For a domain exception you raise deep in a service, register one handler on the FastAPI app (page 6 creates app) that maps it to a response, the way @ControllerAdvice centralizes error translation:
from fastapi import Request
from fastapi.responses import JSONResponse
class EmailTaken(Exception):
pass
@app.exception_handler(EmailTaken)
async def handle_email_taken(request: Request, exc: EmailTaken):
return JSONResponse(status_code=409, content={"detail": "email already registered"})
OpenAPI docs come free
Spring needs springdoc to publish a Swagger UI. FastAPI builds the OpenAPI schema from your routes and models and serves interactive docs at /docs with no extra dependency. The pydantic models you already wrote become the schema.