API Development¶
Learn how to build REST APIs with the FastAPI Boilerplate. This section covers everything you need to create robust, production-ready APIs.
What You'll Learn¶
- Endpoints - Create endpoints with authentication and validation
- Pagination - Add pagination to list endpoints
- Exception Handling - Handle errors with the boilerplate's exception types
- API Versioning - Version your APIs and maintain backward compatibility
Quick Overview¶
Routes are defined in each module's routes.py. The aggregator at interfaces/api/v1/__init__.py mounts each module's router under /api/v1.
# backend/src/modules/user/routes.py
from typing import Annotated, Any
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from ...infrastructure.database.session import async_session
from .schemas import UserCreate, UserRead
from .service import UserService
router = APIRouter(tags=["Users"])
def get_user_service() -> UserService:
return UserService()
@router.post("/", response_model=UserRead, status_code=201)
async def create_user(
user: UserCreate,
db: Annotated[AsyncSession, Depends(async_session)],
user_service: Annotated[UserService, Depends(get_user_service)],
) -> dict[str, Any]:
return await user_service.create(user, db)
The aggregator wires it up:
# backend/src/interfaces/api/v1/__init__.py
from fastapi import APIRouter
from ....modules.user.routes import router as users_router
router = APIRouter(prefix="/v1")
router.include_router(users_router, prefix="/users")
Final URL: POST /api/v1/users/.
Key Features¶
Built-in Authentication¶
Session-based auth with HTTP-only cookies. Pull the current user from infrastructure/auth/dependencies:
from ...infrastructure.auth.dependencies import get_current_user
@router.get("/me", response_model=UserRead)
async def get_profile(
current_user: Annotated[dict[str, Any], Depends(get_current_user)],
) -> dict[str, Any]:
return current_user
For superuser-only endpoints, swap in get_current_superuser. For endpoints gated on a permission a role grants, add require_permissions(...) to the route's dependencies:
from ...infrastructure.auth.authorization import require_permissions
@router.get("/", dependencies=[require_permissions("user.read")])
async def list_users(...): ...
infrastructure/auth/deps.py also exports CurrentPrincipalDep (the crudauth Principal) and CurrentPermissionsDep (the caller's effective permissions) for handlers that need them. See Authentication for the full picture.
Easy Pagination¶
The boilerplate uses FastCRUD's PaginatedListResponse and paginated_response() helper:
from fastcrud import PaginatedListResponse, compute_offset, paginated_response
@router.get("/", response_model=PaginatedListResponse[UserRead])
async def list_users(
db: Annotated[AsyncSession, Depends(async_session)],
user_service: Annotated[UserService, Depends(get_user_service)],
page: int = 1,
items_per_page: int = 10,
) -> dict[str, Any]:
result = await user_service.get_paginated(
skip=compute_offset(page, items_per_page),
limit=items_per_page,
db=db,
)
return paginated_response(crud_data=result, page=page, items_per_page=items_per_page)
Automatic Validation¶
Request bodies, query parameters, and response models are all validated by Pydantic:
@router.post("/", response_model=UserRead)
async def create_user(user: UserCreate): # ← validates input
return await user_service.create(user, db) # ← validates output via response_model
Error Handling¶
Domain errors live in modules/common/exceptions.py, and each feature subclasses them. The app
installs one handler for them, so a route raises (or lets the service raise) and the client gets the
mapped status with a support_id:
from ..common.exceptions import ResourceNotFoundError
@router.get("/{username}", response_model=UserRead)
async def get_user_by_username(
username: str,
db: Annotated[AsyncSession, Depends(async_session)],
user_service: Annotated[UserService, Depends(get_user_service)],
) -> dict[str, Any]:
user = await user_service.get_by_username(username, db)
if user is None:
raise ResourceNotFoundError(f"user {username}")
return user
See Exception Handling for the full catalog.
Architecture¶
HTTP Request
↓
APIRouter (modules/<feature>/routes.py)
↓
Service (modules/<feature>/service.py) — business rules, permission checks
↓
FastCRUD (modules/<feature>/crud.py)
↓
SQLAlchemy Model (modules/<feature>/models.py)
↓
PostgreSQL
The split keeps:
- HTTP concerns (status codes, schemas, dependencies) in
routes.py - Business logic (validation, orchestration) in
service.py - Database I/O in
crud.py
You can mock any layer in tests; you can change one without breaking the others.
Directory Structure¶
backend/src/
├── interfaces/
│ └── api/
│ ├── __init__.py # mounts /api
│ └── v1/
│ └── __init__.py # mounts /v1 + every module's router
├── infrastructure/
│ └── auth/
│ └── routes.py # /api/v1/auth/* (login, OAuth, check-auth)
└── modules/
├── user/routes.py # /api/v1/users/*
├── tier/routes.py # /api/v1/tiers/*
├── rate_limit/routes.py # /api/v1/rate-limits/*
└── api_keys/routes.py # /api/v1/api-keys/*
Auth lives in infrastructure/auth/routes.py instead of in a feature module because authentication is structural — every other feature depends on it.
Mounted Endpoints¶
What ships out of the box:
| Prefix | Source | Notes |
|---|---|---|
POST/GET/PATCH/DELETE /api/v1/users/* |
modules/user/routes.py |
Open create; reads/updates need a session, and a lookup by username returns no email. Listing every user needs the user.read permission |
GET /api/v1/tiers/* |
modules/tier/routes.py |
Authenticated list + lookup by name |
GET/PATCH/DELETE /api/v1/rate-limits/* |
modules/rate_limit/routes.py |
Superuser only |
POST /api/v1/auth/login, logout, check-auth |
infrastructure/auth/routes.py |
Session auth |
GET /api/v1/auth/sessions, DELETE /api/v1/auth/sessions/{handle}, POST /api/v1/auth/logout-all, POST /api/v1/auth/csrf/refresh |
crudauth, mounted in infrastructure/auth/routes.py |
Device management |
POST /api/v1/auth/change-password, GET /api/v1/auth/me |
crudauth's account router, mounted in infrastructure/auth/routes.py |
Session auth; set-password is not mounted |
GET /api/v1/auth/oauth/{provider}, oauth/callback/{provider} |
crudauth router mounted in infrastructure/auth/routes.py |
Google OAuth (configured in infrastructure/auth/setup.py) |
POST/GET/PATCH/DELETE /api/v1/api-keys/* |
modules/api_keys/routes.py |
Authenticated key management |
GET /admin/* |
interfaces/admin/initialize.py |
SQLAdmin UI |
GET /docs, /redoc, /openapi.json |
App factory (protected when gated) | Disabled in production unless ENABLE_DOCS_IN_PRODUCTION=true; when enabled in production or running in staging, requires superuser authentication |
GET /health |
interfaces/main.py |
Liveness: the process is serving |
GET /health/ready |
interfaces/main.py |
Readiness: the overall status, 503 while a critical dependency is unreachable |
What's Next¶
Start with the basics:
- Endpoints - Common patterns for new routes
- Pagination - List endpoints with paged responses
- Exception Handling - The boilerplate's exception model
- API Versioning - Versioning strategy
Then go deeper:
- Database Schemas - Pydantic shapes used in routes
- CRUD Operations - The data layer below the service