FastAPI¶
Overview¶
FastAPI is a modern, high-performance web framework for building APIs with Python. It serves as the core application framework for the Quill Medical backend, handling all HTTP requests, authentication, routing, and business logic.
Why FastAPI?¶
Performance¶
- High Speed: One of the fastest Python frameworks available, comparable to Node.js and Go
- Async Support: Built on Starlette for async/await support, enabling high concurrency
- Production Ready: Powers APIs at scale for companies like Microsoft, Uber, and Netflix
Developer Experience¶
- Type Hints: Leverages Python type hints for automatic validation and documentation
- Auto Documentation: Generates interactive API docs (Swagger UI, ReDoc) automatically
- Editor Support: Excellent IDE autocompletion and type checking
- Minimal Boilerplate: Write less code to accomplish more
Modern Features¶
- Automatic Validation: Request/response validation using Pydantic models
- Dependency Injection: Clean, testable code with built-in dependency injection
- Standards-Based: Based on OpenAPI (formerly Swagger) and JSON Schema standards
- Security: Built-in security utilities for OAuth2, JWT, API keys, etc.
Python 3.13+ Features¶
- Native support for modern Python features
- Type annotations and pattern matching
- Async/await and asyncio
- Modern syntax and performance improvements
Our Implementation¶
Application Structure¶
The backend is organised in backend/app/:
app/
├── main.py # Application entry point, route definitions
├── config.py # Configuration and environment variables
├── models.py # SQLAlchemy ORM models (User, Organization, etc.)
├── security.py # Authentication, JWT, password hashing, CSRF, TOTP
├── fhir_client.py # FHIR integration
├── ehrbase_client.py # OpenEHR integration
├── messaging.py # Messaging CQRS coordination layer
├── organisations.py # Organisation access control helpers
├── patient_records.py # File-based patient record management
├── logging_config.py # Structured JSON logging configuration
├── push.py # Web push notification endpoints
├── push_send.py # Push notification sending logic
├── db/
│ ├── __init__.py # Database module exports
│ └── auth_db.py # Auth database engine and session management
├── cbac/
│ ├── __init__.py # CBAC module exports
│ ├── competencies.py # Competency definitions from YAML
│ ├── base_professions.py # Base profession definitions from YAML
│ └── decorators.py # has_competency(), FastAPI dependencies
├── features/
│ ├── __init__.py # Feature-gating utilities (requires_feature dependency)
│ └── teaching/ # Teaching feature module
├── system_permissions/
│ ├── __init__.py # Permission module exports
│ ├── permissions.py # Permission types and hierarchy validation
│ └── decorators.py # requires_staff(), requires_admin() dependencies
├── schemas/
│ ├── __init__.py # Schema module exports
│ ├── auth.py # Authentication request/response models
│ ├── cbac.py # CBAC request/response models
│ ├── features.py # Organisation feature toggle models
│ ├── letters.py # Letter/correspondence models
│ └── messaging.py # Messaging request/response models
└── utils/
└── colors.py # Avatar gradient colour utilities
Key Components¶
Main Application (main.py)¶
from fastapi import FastAPI
app = FastAPI(
title="Quill API",
docs_url="/api/docs", # Swagger UI (dev only)
redoc_url="/api/redoc" # ReDoc (dev only)
)
router = APIRouter(prefix="/api")
Request/Response Models (Pydantic)¶
from pydantic import BaseModel
class LetterIn(BaseModel):
"""Input model for creating a letter."""
title: str
body: str
author_name: str | None = None
author_email: str | None = None
Route Definitions¶
@router.get("/patients")
def list_patients(u: User = DEP_CURRENT_USER):
"""List all patients from FHIR server."""
patients = list_fhir_patients()
return {"patients": patients}
@router.post(
"/patients/{patient_id}/letters",
dependencies=[DEP_REQUIRE_ROLES_CLINICIAN, DEP_REQUIRE_CSRF]
)
def create_letter(patient_id: str, letter: LetterIn):
"""Create a new clinical letter."""
result = create_letter_composition(
patient_id=patient_id,
title=letter.title,
body=letter.body
)
return result
API Architecture¶
API Prefix¶
All API endpoints are prefixed with /api:
/api/auth/*- Authentication endpoints (login, register, TOTP, refresh, logout)/api/users/*- User management (admin CRUD, profile)/api/patients/*- Patient management (backed by FHIR)/api/patients/{id}/letters/*- Clinical letters (backed by OpenEHR)/api/patients/{id}/conversations/*- Patient-scoped conversations/api/patients/{id}/invite-external- External access invitations/api/patients/{id}/external-access/*- External access management/api/conversations/*- Messaging (conversations and messages)/api/organizations/*- Organisation management (admin)/api/cbac/*- Competency-based access control/api/push/*- Web push notifications/api/health- Service health check
Authentication & Security¶
JWT-Based Authentication¶
# Login with credentials and optional TOTP
POST /api/auth/login
{
"username": "user@example.com",
"password": "password123",
"totp_code": "123456" # Optional, required if 2FA enabled
}
# Response body
{
"detail": "ok",
"user": { "username": "...", "roles": [...] }
}
# JWT access/refresh tokens and CSRF token set as HTTP-only cookies
Protected Endpoints¶
# Requires valid JWT
@router.get("/patients")
def list_patients(u: User = DEP_CURRENT_USER):
pass
# Requires clinician role
@router.post("/patients/{id}/letters")
def create_letter(
patient_id: str,
letter: LetterIn,
u: User = DEP_REQUIRE_ROLES_CLINICIAN,
):
pass
CSRF Protection¶
# POST/PUT/DELETE require CSRF token
@router.post(
"/patients/{id}/demographics",
dependencies=[DEP_REQUIRE_CSRF]
)
def update_demographics(...):
pass
Permission-Based Access Control¶
Defence-in-depth architecture¶
The backend implements a multi-layered permission system:
- Authentication layer: JWT token validation
- Permission layer: Role/permission checks
- Resource layer: Ownership and assignment validation
- Audit layer: All access attempts logged
Permission hierarchy¶
# 4-level hierarchy for permission checks: patient < staff < admin < superadmin
PERMISSION_LEVELS = ["patient", "staff", "admin", "superadmin"]
# All valid permission values (includes non-hierarchical external types)
ALL_PERMISSIONS = [
"patient", "external_hcp", "patient_advocate",
"staff", "admin", "superadmin",
]
# check_permission_level(user_permission, required_permission)
# Returns True if user meets or exceeds required level
# External types (external_hcp, patient_advocate) treated as patient level
Usage in endpoints¶
# Require minimum permission level
@router.get("/admin/users")
def list_users(
user: User = Depends(get_current_user)
):
if user.system_permissions not in ["admin", "superadmin"]:
raise HTTPException(403, "Insufficient permissions")
# ... list users
# CBAC-protected endpoint
@router.post(
"/patients/{id}/letters",
dependencies=[DEP_REQUIRE_CSRF]
)
def create_letter(
patient_id: str,
letter: LetterIn,
u: User = DEP_REQUIRE_ROLES_CLINICIAN,
):
# ... create letter
Security principles¶
- Source of truth: Backend always validates permissions
- No trust: Never trust client-side permission state
- Fail-safe: Default deny, explicit grants required
- Consistent errors: Generic 403 messages prevent information disclosure
- Audit logging: All permission checks logged for compliance
Integration with frontend¶
The backend permission system works with frontend route guards:
- Backend: Enforces access rules (security)
- Frontend: Hides inaccessible features (UX)
- Both: Use same permission hierarchy
- Defence in depth: Multiple validation layers
Dependency Injection¶
FastAPI's dependency injection provides clean, testable code. The backend uses pre-built dependency constants:
# Standard dependency constants (defined in main.py)
DEP_GET_SESSION # Database session via get_auth_db
DEP_CURRENT_USER # Authenticated user from JWT cookie
DEP_REQUIRE_ROLES_CLINICIAN # Clinician role gate
DEP_REQUIRE_CSRF # CSRF token validation (mutating endpoints)
# Use in routes
@router.get("/profile")
def get_profile(u: User = DEP_CURRENT_USER):
return u
API Documentation¶
Swagger UI¶
Interactive API documentation available at /api/docs:
- Test endpoints directly in the browser
- See request/response schemas
- Try authentication flows
- View all available operations
ReDoc¶
Alternative documentation view at /api/redoc:
- Three-panel layout
- Better for reading and reference
- Search functionality
- Code samples
OpenAPI Spec¶
Raw OpenAPI JSON at /api/openapi.json:
- Used by API clients and tools
- Auto-generated from code
- Includes all schemas and operations
Request/Response Handling¶
Automatic Validation¶
class PatientDemographics(BaseModel):
given_name: str
family_name: str
date_of_birth: str
sex: str
@router.put("/patients/{patient_id}/demographics")
def update_demographics(
patient_id: str,
demographics: PatientDemographics # Auto-validated
):
# If we reach here, data is valid
result = update_fhir_patient(patient_id, demographics.model_dump())
return result
Error Handling¶
from fastapi import HTTPException
@router.get("/patients/{patient_id}")
def get_patient(patient_id: str):
patient = read_fhir_patient(patient_id)
if patient is None:
raise HTTPException(
status_code=404,
detail="Patient not found"
)
return patient
Response Models¶
class PatientResponse(BaseModel):
patient_id: str
name: str
date_of_birth: str
@router.get("/patients/{id}", response_model=PatientResponse)
def get_patient(id: str) -> PatientResponse:
# Response automatically validated and serialized
return PatientResponse(...)
Development Features¶
Hot Reload¶
FastAPI automatically reloads on code changes in development:
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
Async Support¶
@router.get("/async-endpoint")
async def async_operation():
# Can use async libraries
result = await some_async_function()
return result
Background Tasks¶
from fastapi import BackgroundTasks
@router.post("/send-notification")
def send_notification(background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, "user@example.com")
return {"message": "Notification queued"}
Testing¶
FastAPI provides excellent testing support:
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_list_patients():
response = client.get("/api/patients")
assert response.status_code == 200
assert "patients" in response.json()