backend / web frameworks / fastapi / 00_fastapi_overview.md

FastAPI Guide for Aventur Project

4 interview angles 11 min read source

FastAPI Guide for Aventur Project

This document explains FastAPI concepts and how they’re used in the Aventur backend project.

Table of Contents

  1. FastAPI Overview
  2. FastAPI Imports Explained
  3. Project Structure
  4. Application Setup
  5. Routers and Endpoints
  6. Dependency Injection
  7. Request/Response Models
  8. Authentication & Permissions
  9. Exception Handling
  10. Middleware
  11. 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.status module

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 version
  • api/schemas/: Pydantic models for request/response validation
  • api/security/: Authentication and permission logic
  • app/: 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):

  1. CORS Middleware: Handles cross-origin requests
  2. GZip Middleware: Compresses responses
  3. 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 schema
  • mount(): 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 (from dependency_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:

  1. First added = Outermost (runs first on request, last on response)
  2. 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, testing environments
  • 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 @inject decorator 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:

  1. Type Safety: Extensive use of type hints for validation
  2. Separation: Clear separation between API, business logic, and data layers
  3. Dependency Injection: Both FastAPI Depends and dependency_injector
  4. Versioning: Multiple API versions with different mounting strategies
  5. Security: JWT-based authentication with role-based permissions
  6. Validation: Pydantic models for request/response validation
  7. Error Handling: Custom exceptions with global handlers
  8. Documentation: Automatic OpenAPI schema generation

For more examples, see:

  • api/routers/api_v2/support/support_router.py - File upload example
  • api/routers/api_v2/clients/clients_router.py - Standard CRUD example
  • api/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.