FastAPI Guide for Aventur Project
This document explains FastAPI concepts and how they’re used in the Aventur backend project.
Table of Contents
- FastAPI Overview
- FastAPI Imports Explained
- Project Structure
- Application Setup
- Routers and Endpoints
- Dependency Injection
- Request/Response Models
- Authentication & Permissions
- Exception Handling
- Middleware
- API Versioning
FastAPI Overview
FastAPI is a modern, fast web framework for building APIs with Python. It’s built on top of:
- Starlette - for web framework functionality
- Pydantic - for data validation using Python type hints
- OpenAPI - automatic API documentation generation
Key Features
- Type hints: FastAPI uses Python type hints for validation and documentation
- Automatic validation: Request/response data is validated automatically
- OpenAPI/Swagger docs: Interactive API documentation generated automatically
- Async support: Native support for async/await
- Dependency injection: Built-in dependency injection system
FastAPI Imports Explained
Common FastAPI Imports
from fastapi import Depends, File, Form, HTTPException, UploadFile
Depends
Used for dependency injection. It allows you to declare dependencies that FastAPI will resolve and inject into your route handlers.
Example:
from fastapi import Depends
from api.common.db_injection import AsyncSessionInjection
async def get_client(
client_id: int,
session: AsyncSessionInjection, # Injected database session
):
# session is automatically provided by FastAPI
return await session.get(Client, client_id)
In this project:
- Used to inject database sessions (
AsyncSessionInjection) - Used to inject services via dependency injection container
- Used for permission checks (
AUTHENTICATED_PERMISSIONS) - Used for request validation functions
File
Used to declare file upload parameters in form data. Works with UploadFile.
Example:
from fastapi import File, UploadFile
@router.post("/upload")
async def upload_file(
file: UploadFile = File(...), # Required file
# or
file: UploadFile | None = File(None), # Optional file
):
content = await file.read()
return {"filename": file.filename, "size": len(content)}
In this project:
- Used in support router for attachment uploads
- Validates file types and sizes before processing
Form
Used to declare form data fields (for application/x-www-form-urlencoded or multipart/form-data).
Example:
from fastapi import Form
@router.post("/feedback")
async def submit_feedback(
name: str = Form(...), # Required form field
email: str = Form(...),
message: str = Form(..., min_length=1),
):
return {"name": name, "email": email}
In this project:
- Used in support router for form data validation
- Combined with Pydantic models for validation
HTTPException
Used to raise HTTP exceptions with specific status codes and error messages.
Example:
from fastapi import HTTPException
from starlette import status
@router.get("/client/{client_id}")
async def get_client(client_id: int):
client = await find_client(client_id)
if not client:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Client {client_id} not found"
)
return client
In this project:
- Used for error handling in route handlers
- Used in validation functions to return proper error responses
- Status codes from
starlette.statusmodule
UploadFile
A class representing an uploaded file. Provides file metadata and content.
Example:
from fastapi import UploadFile, File
@router.post("/upload")
async def upload(attachment: UploadFile = File(None)):
if attachment:
content = await attachment.read()
filename = attachment.filename
content_type = attachment.content_type
# Process file...
In this project:
- Used for file uploads in support tickets
- Validated for type and size before processing
Other Common FastAPI Imports
FastAPI
The main application class.
from fastapi import FastAPI
app = FastAPI(openapi_url="/api/openapi.json")
APIRouter
Used to organize routes into groups.
from fastapi import APIRouter
router = APIRouter(prefix="/clients", tags=["Clients"])
Query
Used to declare query parameters with validation.
from fastapi import Query
@router.get("/items")
async def get_items(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
):
return {"skip": skip, "limit": limit}
Request
Represents the HTTP request object.
from fastapi import Request
@router.get("/")
async def get_request_info(request: Request):
return {
"method": request.method,
"url": str(request.url),
"headers": dict(request.headers)
}
Project Structure
backend/
├── src/
│ ├── api/ # FastAPI application layer
│ │ ├── main.py # Main FastAPI app
│ │ ├── routers/ # API route definitions
│ │ │ ├── api_v1/ # API version 1 routes
│ │ │ ├── api_v2/ # API version 2 routes
│ │ │ ├── auth/ # Authentication routes
│ │ │ └── devtools_router.py
│ │ ├── schemas/ # Pydantic request/response models
│ │ │ ├── requests/ # Request schemas
│ │ │ └── responses/ # Response schemas
│ │ ├── security/ # Authentication & permissions
│ │ ├── middleware/ # Custom middleware
│ │ ├── exceptions/ # Exception handlers
│ │ └── common/ # Shared utilities
│ ├── app/ # Business logic layer
│ │ ├── clients/ # Client domain logic
│ │ ├── cases/ # Case domain logic
│ │ └── ... # Other domains
│ ├── database/ # Database layer
│ ├── gateway_accessors/ # External service integrations
│ └── common/ # Shared utilities
└── tests/ # Test suite
Key Directories
api/routers/: Contains all route definitions organized by API versionapi/schemas/: Pydantic models for request/response validationapi/security/: Authentication and permission logicapp/: Business logic and domain services (separated from API layer)database/: Database models, repositories, and queries
Application Setup
Main Application (api/main.py)
The main FastAPI application is created in api/main.py:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(openapi_url="/api/openapi.json")
Middleware Stack
Middleware is added in order (first added = outermost layer):
- CORS Middleware: Handles cross-origin requests
- GZip Middleware: Compresses responses
- Authentication Middleware: Validates JWT tokens
app.add_middleware(CORSMiddleware, allow_origins=["*"], ...)
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(AuthenticationMiddleware, backend=BearerTokenAuthBackend())
Exception Handlers
Global exception handlers for custom exceptions:
app.add_exception_handler(ServiceException, service_exception_handler)
Router Registration
Routers are registered with prefixes:
# API v1: Included directly (routes appear in main OpenAPI schema)
app.include_router(api_router, prefix="/api/v1")
# API v2: Mounted as sub-application (separate OpenAPI schema)
app.mount("/api/v2", api_v2)
app.mount("/auth", auth_api)
Key Difference:
include_router(): Routes are merged into main app’s OpenAPI schemamount(): Creates a sub-application with its own OpenAPI schema
Routers and Endpoints
Custom Router Class
The project uses a custom JarvisAPIRouter that extends FastAPI’s router:
Location: api/routers/api_router.py
Features:
- Custom route handler that sets up
AppSession - Permission extraction from route dependencies
- Automatic trailing slash support
from api.routers.api_router import JarvisAPIRouter
router = JarvisAPIRouter(prefix="/support", tags=["Support"])
Endpoint Definition Pattern
@router.post(
"/ticket",
response_model=SupportTicketResponse, # Response validation
status_code=status.HTTP_201_CREATED, # HTTP status code
dependencies=[AUTHENTICATED_PERMISSIONS], # Permission check
summary="Create a support ticket", # OpenAPI summary
description="Creates a Jira ticket...", # OpenAPI description
)
@inject # Dependency injection decorator
async def create_support_ticket(
request: SupportTicketRequest = Depends(validate_support_request),
attachment: UploadFile | None = File(None),
support_service: SupportService = Depends(Provide[Container.support_services.support_service]),
) -> SupportTicketResponse:
# Business logic here
return SupportTicketResponse(...)
Route Decorators
@router.get(),@router.post(),@router.put(),@router.patch(),@router.delete()@inject: Enables dependency injection (fromdependency_injector)
Path Parameters
@router.get("/{client_id}/health-score")
async def get_health_score(client_id: int): # Type-validated path parameter
return await service.get_health_score(client_id)
Query Parameters
from fastapi import Query
@router.get("/health-scores")
async def get_health_scores(
start_date: date | None = None, # Optional query param
end_date: date | None = None,
):
return await service.get_health_scores(start_date, end_date)
Dependency Injection
Two Dependency Systems
The project uses both FastAPI’s Depends and dependency_injector:
1. FastAPI Depends
For simple dependencies like database sessions:
from api.common.db_injection import AsyncSessionInjection
async def get_client(
client_id: int,
session: AsyncSessionInjection, # FastAPI Depends
):
...
2. Dependency Injector Container
For complex service dependencies:
from dependency_injector.wiring import Provide, inject
from common.containers.core_containers import Container
@inject
async def create_ticket(
support_service: SupportService = Depends(Provide[Container.support_services.support_service]),
):
...
Container Setup:
container = Container()
container.config.from_dict(settings.model_dump())
container.wire(packages=["api.routers.api_v1", ...])
app.container = container
Permission Dependencies
Permissions are applied as dependencies:
from api.security.permission_roles import AUTHENTICATED_PERMISSIONS
@router.post(
"/ticket",
dependencies=[AUTHENTICATED_PERMISSIONS], # Applied to entire route
)
async def create_ticket(...):
...
Request/Response Models
Pydantic Models
Request and response validation uses Pydantic models:
Request Models
Location: api/routers/api_v2/*/..._requests.py or api/schemas/requests/
from pydantic import BaseModel, EmailStr, Field
class SupportTicketRequest(BaseModel):
name: str = Field(..., min_length=1)
email: EmailStr
message: str = Field(..., min_length=1, max_length=1000)
Response Models
Location: api/routers/api_v2/*/..._responses.py or api/schemas/responses/
class SupportTicketResponse(BaseModel):
issue_key: str
issue_id: str
message: str
Using Models in Routes
@router.post(
"/ticket",
response_model=SupportTicketResponse, # Validates response
)
async def create_ticket(
request: SupportTicketRequest, # Validates request body
) -> SupportTicketResponse:
...
Form Data Validation
For form data (multipart/form-data), use Form with validation functions:
def validate_support_request(
name: str = Form(...),
email: str = Form(...),
message: str = Form(...),
) -> SupportTicketRequest:
try:
return SupportTicketRequest(name=name, email=email, message=message)
except ValidationError as e:
raise HTTPException(status_code=422, detail=...)
@router.post("/ticket")
async def create_ticket(
request: SupportTicketRequest = Depends(validate_support_request),
):
...
Authentication & Permissions
Authentication Middleware
Location: api/middleware/authentication_middleware.py
The BearerTokenAuthBackend validates JWT tokens and sets user context:
class BearerTokenAuthBackend(AuthenticationBackend):
async def authenticate(self, request: Request):
# Validates JWT token
# Sets request.state.user, request.state.permissions
return AuthCredentials(scopes), user_entity
Permission System
Location: api/security/security.py
The Permissions class checks user roles:
class Permissions:
def __init__(self, permissions: List[str]):
self.permissions = HashableSecurityScopes(permissions)
async def __call__(self, request: Request, ...):
# Checks if user has required permissions
# Raises exception if not authorized
Permission Roles
Location: api/security/permission_roles.py
Pre-defined permission sets:
AUTHENTICATED_PERMISSIONS = Permissions([UserRole.Client, UserRole.Adviser, ...])
DEFAULT_PERMISSIONS = Permissions([...])
Using Permissions
from api.security.permission_roles import AUTHENTICATED_PERMISSIONS
@router.post(
"/ticket",
dependencies=[AUTHENTICATED_PERMISSIONS], # Applied to route
)
async def create_ticket(...):
...
Accessing User in Routes
User information is available via request.state:
from fastapi import Request
@router.get("/me")
async def get_current_user(request: Request):
user = request.state.user
return {"id": user.id, "email": user.email}
Exception Handling
Custom Exceptions
Location: api/exceptions/
Custom exception classes extend ServiceException:
class ServiceException(Exception):
error_type: HTTPStatus
error_message: str
Exception Handlers
Location: api/exceptions/exception_handlers.py
Global exception handler:
async def service_exception_handler(request: Request, exc: ServiceException):
return JSONResponse(
status_code=exc.error_type.value,
content={"detail": exc.error_message}
)
Registration:
app.add_exception_handler(ServiceException, service_exception_handler)
Route-Level Exception Handlers
Sub-applications can have their own handlers:
api_v2.add_exception_handler(ServiceException, service_exception_handler)
Raising Exceptions
from fastapi import HTTPException
from starlette import status
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Resource not found"
)
Middleware
Custom Middleware
Location: api/middleware/
Authentication Middleware
- Validates JWT tokens
- Sets user context in
request.state
Devtools Middleware
- Development-only middleware
- Additional debugging features
Middleware Order
Middleware executes in reverse order of addition:
- First added = Outermost (runs first on request, last on response)
- Last added = Innermost (runs last on request, first on response)
app.add_middleware(CORSMiddleware, ...) # Outermost
app.add_middleware(GZipMiddleware, ...) # Middle
app.add_middleware(AuthenticationMiddleware, ...) # Innermost
API Versioning
API v1
Location: api/routers/api_v1/
- Routers included directly:
app.include_router(api_router, prefix="/api/v1") - Routes appear in main OpenAPI schema
- Uses
JarvisAPIRouter
API v2
Location: api/routers/api_v2/
- Mounted as sub-application:
app.mount("/api/v2", api_v2) - Separate FastAPI instance with own OpenAPI schema
- More modern structure with dedicated request/response models per router
Auth API
Location: api/routers/auth/
- Mounted sub-application:
app.mount("/auth", auth_api) - Independent authentication endpoints
Devtools
Location: api/routers/devtools_router.py
- Only available in
ci,local,testingenvironments - Mounted sub-application:
app.mount("/devtools", devtools)
Best Practices in This Project
1. Separation of Concerns
- API Layer (
api/): Routes, schemas, security - Business Logic (
app/): Services, domain logic - Database (
database/): Models, repositories, queries
2. Route Structure
@router.post(
"/endpoint",
response_model=ResponseModel,
status_code=status.HTTP_201_CREATED,
dependencies=[PERMISSIONS],
summary="Brief description",
description="Detailed description",
)
@inject
async def endpoint_handler(
path_param: int,
query_param: str | None = None,
body: RequestModel,
service: Service = Depends(Provide[Container.service]),
) -> ResponseModel:
# No business logic here - delegate to service
result = await service.do_something(body)
return ResponseModel(...)
3. Request Validation
- Use Pydantic models for JSON body validation
- Use
Form+ validation functions for form data - Validate file uploads (type, size) before processing
4. Error Handling
- Use custom exceptions (
ServiceException) - Register global exception handlers
- Return appropriate HTTP status codes
5. Dependency Injection
- Use
@injectdecorator for dependency injection - Inject services via container:
Depends(Provide[Container.service]) - Inject database sessions:
AsyncSessionInjection
6. Permissions
- Apply permissions as route dependencies
- Use pre-defined permission sets from
permission_roles.py - Check permissions in middleware, not in route handlers
Common Patterns
File Upload with Validation
@router.post("/upload")
async def upload_file(
attachment: UploadFile | None = File(None),
):
if attachment:
# Validate content type
if attachment.content_type not in ALLOWED_TYPES:
raise HTTPException(status_code=415, detail="Invalid file type")
# Validate size
content = await attachment.read()
if len(content) > MAX_SIZE:
raise HTTPException(status_code=413, detail="File too large")
# Process file
...
Path Parameter Validation
@router.get("/{client_id}/data")
async def get_data(client_id: int): # FastAPI validates int automatically
if client_id < 1:
raise HTTPException(status_code=400, detail="Invalid client_id")
...
Query Parameter with Defaults
@router.get("/items")
async def get_items(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
):
...
OpenAPI Documentation
FastAPI automatically generates OpenAPI documentation:
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc - OpenAPI JSON:
http://localhost:8000/api/openapi.json
Customizing Documentation
@router.post(
"/endpoint",
summary="Short description",
description="Long description with details",
response_description="What the response contains",
tags=["Custom Tag"],
)
async def endpoint(...):
...
Testing
Test Structure
Location: tests/acceptance/test_permissions.py
Tests use httpx.AsyncClient to make requests:
async def test_endpoint(steps):
response = await steps.client.get("/api/v1/endpoint")
assert response.status_code == 200
Permission Tests
The project includes comprehensive permission tests that verify:
- All endpoints are covered in permission configuration
- Permission enforcement works correctly
- Different user roles have appropriate access
Summary
FastAPI in this project follows these key principles:
- Type Safety: Extensive use of type hints for validation
- Separation: Clear separation between API, business logic, and data layers
- Dependency Injection: Both FastAPI
Dependsanddependency_injector - Versioning: Multiple API versions with different mounting strategies
- Security: JWT-based authentication with role-based permissions
- Validation: Pydantic models for request/response validation
- Error Handling: Custom exceptions with global handlers
- Documentation: Automatic OpenAPI schema generation
For more examples, see:
api/routers/api_v2/support/support_router.py- File upload exampleapi/routers/api_v2/clients/clients_router.py- Standard CRUD exampleapi/routers/api_router.py- Custom router implementation
Interview angle
- “Why FastAPI over Flask or Django?” - type hints drive validation, serialisation and OpenAPI generation, so one declaration does three jobs. It’s ASGI-native, so async I/O is real concurrency rather than threads. Django still wins for a batteries-included admin and ORM-heavy CRUD.
- “What does FastAPI actually give you over Starlette?” - Starlette is the ASGI framework underneath; FastAPI adds Pydantic-driven request/response validation, dependency injection and automatic OpenAPI. Anything routing- or middleware-shaped is Starlette.
- “How does it generate docs?” - from your type annotations and Pydantic models. That’s why the docs stay accurate: they’re derived from the same declarations the validation uses, so they can’t drift the way hand-written docs do.
- “Any downsides?” - the DI system is request-scoped and framework-bound, so background workers need separate wiring; and it’s less opinionated than Django, so project structure is your problem.