laranevans.com
Guides / Python and FastAPI for Spring Boot Engineers / 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.

Next: add persistence and run the service.