FastAPI Kit
Overview
Purpose: Provide seamless integration between Portico and FastAPI with dependency injection, route decorators, exception handling, and middleware for building web applications.
Key Features:
- Type-safe dependency injection for services and authentication
- Route protection decorators for auth and permissions
- Automatic exception handling (converts Portico exceptions to HTTP responses)
- Request logging middleware with structured logging
- Jinja2 template integration helpers
- Support for both cookie and Bearer token authentication
Dependencies:
- Injected services: None (provides access to all kit services)
- Port dependencies: None (utility kit)
- Note: This is a utility kit that doesn't follow the standard kit pattern (no service, repository, or models). It provides FastAPI integration helpers.
Quick Start
from fastapi import FastAPI, Request
from portico import compose
from portico.kits.fastapi import (
Dependencies,
register_exception_handlers,
RequestLoggingMiddleware,
)
# Create Portico app
app = compose.webapp(
database_url="postgresql://localhost/myapp",
kits=[
compose.user(),
compose.auth(session_secret="your-secret-key"),
compose.rbac(),
]
)
# Create FastAPI app
fastapi_app = FastAPI()
# Register Portico exception handlers
register_exception_handlers(fastapi_app)
# Add request logging middleware
fastapi_app.add_middleware(RequestLoggingMiddleware)
# Create dependencies helper
deps = Dependencies(app)
# Protected route with dependency injection
@fastapi_app.get("/dashboard")
async def dashboard(current_user = deps.current_user):
return {"email": current_user.email}
Core Concepts
Dependency Injection with Dependencies Class
The Dependencies class provides type-safe access to common dependencies:
from portico.kits.fastapi import Dependencies
deps = Dependencies(app)
@fastapi_app.get("/users")
async def list_users(
current_user = deps.current_user, # Authenticated user (raises 401 if not auth)
session = deps.session, # Database session
):
# Access services directly
user_service = deps.user_service
users = await user_service.list_users()
return {"users": users}
@fastapi_app.get("/public")
async def public_page(
user = deps.optional_user # None if not authenticated, no 401
):
if user:
return {"message": f"Welcome back, {user.email}"}
return {"message": "Welcome, guest"}
Available dependencies:
deps.current_user- Authenticated user (raises 401)deps.optional_user- User or None (no error)deps.session- Database sessiondeps.user_service- UserManagementServicedeps.group_service- GroupManagementServicedeps.auth_service- AuthenticationServicedeps.rbac_service- AuthorizationServicedeps.webapp- WebApp instance
Route Protection Decorators
Simplify auth and permission checking with decorators:
from portico.kits.fastapi import (
requires_auth,
requires_permission,
requires_role,
requires_group_permission,
)
# Require authentication
@fastapi_app.get("/dashboard")
@requires_auth(app)
async def dashboard(current_user):
return {"email": current_user.email}
# Require specific permission
@fastapi_app.delete("/users/{user_id}")
@requires_permission(app, "users.delete")
async def delete_user(user_id: UUID, current_user):
await deps.user_service.delete_user(user_id)
return {"status": "deleted"}
# Require role
@fastapi_app.get("/admin")
@requires_role(app, "admin")
async def admin_panel(current_user):
return {"message": "Admin access"}
# Require multiple acceptable roles
@fastapi_app.get("/staff")
@requires_role(app, ["admin", "moderator", "staff"])
async def staff_dashboard(current_user):
return {"message": "Staff access"}
# Require group-specific permission
@fastapi_app.post("/groups/{group_id}/files")
@requires_group_permission(app, "files.write")
async def upload_file(group_id: UUID, file: UploadFile, current_user):
return await file_service.upload(group_id, file)
Automatic Exception Handling
Convert Portico domain exceptions to proper HTTP responses:
from portico.kits.fastapi import register_exception_handlers
from portico.exceptions import (
AuthenticationError,
AuthorizationError,
ResourceNotFoundError,
)
# Register handlers once at startup
register_exception_handlers(fastapi_app)
# Now exceptions automatically convert to HTTP responses
@fastapi_app.get("/users/{user_id}")
async def get_user(user_id: UUID):
user = await user_service.get_by_id(user_id)
if not user:
# Automatically returns 404 with proper JSON
raise ResourceNotFoundError(f"User {user_id} not found")
return user
@fastapi_app.post("/admin/action")
async def admin_action(current_user = deps.current_user):
if not await rbac_service.check_permission(current_user.id, "admin.action"):
# Automatically returns 403 with proper JSON
raise AuthorizationError("Admin permission required")
# Perform action
Exception mappings:
AuthenticationError→ 401 UnauthorizedAuthorizationError→ 403 ForbiddenResourceNotFoundError→ 404 Not FoundValidationError→ 400 Bad RequestConflictError→ 409 ConflictRateLimitError→ 429 Too Many RequestsPorticoError→ Uses exception's status_code (default 500)
Request Logging Middleware
Add structured logging with request IDs:
from portico.kits.fastapi import RequestLoggingMiddleware
fastapi_app.add_middleware(RequestLoggingMiddleware)
# Now all requests are logged with:
# - Unique request ID
# - HTTP method and path
# - Response status code
# - Errors (if any)
@fastapi_app.get("/api/data")
async def get_data(request: Request):
# request.state.request_id is available
logger.info("processing_data", request_id=request.state.request_id)
return {"data": "value"}
Logs example:
INFO: http_request method=GET path=/api/data request_id=abc-123
INFO: processing_data request_id=abc-123
INFO: http_response method=GET path=/api/data status_code=200 request_id=abc-123
Template Integration
Use Portico features in Jinja2 templates:
from fastapi.templating import Jinja2Templates
from portico.kits.fastapi import setup_template_globals, create_template_context
templates = Jinja2Templates(directory="templates")
setup_template_globals(templates, app)
@fastapi_app.get("/dashboard")
async def dashboard(request: Request, user = deps.current_user):
context = create_template_context(
request,
user=user,
page_title="Dashboard"
)
return templates.TemplateResponse("dashboard.html", context)
In templates:
<!DOCTYPE html>
<html>
<head>
<title>{{ page_title }}</title>
{{ app.render_assets('head')|safe }}
</head>
<body>
<h1>Welcome, {{ user.email }}</h1>
<p>App version: {{ app.version }}</p>
</body>
</html>
Configuration
This kit has no configuration. It's a collection of utilities that integrate Portico with FastAPI.
Usage Examples
Example 1: Complete FastAPI Application Setup
from contextlib import asynccontextmanager
from fastapi import FastAPI
from portico import compose
from portico.kits.fastapi import (
Dependencies,
register_exception_handlers,
RequestLoggingMiddleware,
)
# Create Portico app
portico_app = compose.webapp(
database_url="postgresql://localhost/myapp",
kits=[
compose.user(),
compose.auth(session_secret="your-secret"),
compose.group(),
compose.rbac(),
]
)
# Lifespan for startup/shutdown
@asynccontextmanager
async def lifespan(app: FastAPI):
await portico_app.initialize()
yield
await portico_app.close()
# Create FastAPI app
app = FastAPI(lifespan=lifespan)
# Register exception handlers and middleware
register_exception_handlers(app)
app.add_middleware(RequestLoggingMiddleware)
# Create dependencies
deps = Dependencies(portico_app)
# Routes
@app.get("/")
async def home():
return {"message": "Welcome to Portico"}
@app.post("/login")
async def login(email: str, password: str):
result = await deps.auth_service.authenticate(email, password)
return {"token": result.token}
@app.get("/dashboard")
async def dashboard(user = deps.current_user):
return {"email": user.email}
Example 2: Protected Routes with RBAC
from portico.kits.fastapi import requires_permission, requires_role
from portico.ports.user import CreateUserRequest
# Admin-only route
@app.get("/admin/users")
@requires_role(portico_app, "admin")
async def list_all_users(current_user):
users = await deps.user_service.list_users()
return {"users": users, "count": len(users)}
# Permission-based route
@app.post("/users")
@requires_permission(portico_app, "users.create")
async def create_user(
user_data: CreateUserRequest,
current_user
):
user = await deps.user_service.create_user(user_data)
return {"user_id": str(user.id)}
# Group-specific permission
@app.get("/groups/{group_id}/members")
@requires_group_permission(portico_app, "members.read")
async def list_members(group_id: UUID, current_user):
members = await deps.group_service.get_members(group_id)
return {"members": members}
Example 3: Custom Authentication Flow
from fastapi import Response
from portico.exceptions import AuthenticationError
@app.post("/login")
async def login(
response: Response,
email: str,
password: str
):
try:
result = await deps.auth_service.authenticate(email, password)
# Set cookie
response.set_cookie(
key="access_token",
value=result.token,
httponly=True,
secure=True,
samesite="lax",
max_age=3600
)
return {
"success": True,
"user": {
"id": str(result.user.id),
"email": result.user.email
}
}
except AuthenticationError as e:
# Exception handler converts to 401 automatically
raise
@app.post("/logout")
async def logout(response: Response, user = deps.optional_user):
if user:
# Get token from cookie or header
token = response.cookies.get("access_token")
if token:
await deps.auth_service.logout(token)
response.delete_cookie("access_token")
return {"success": True}
Example 4: API with Error Handling
from portico.exceptions import (
ResourceNotFoundError,
ValidationError,
AuthorizationError,
)
@app.get("/api/users/{user_id}")
async def get_user(user_id: UUID, current_user = deps.current_user):
# Check permission
if not await deps.rbac_service.check_permission(
current_user.id, "users.read"
):
raise AuthorizationError("Cannot read user data")
# Get user
user = await deps.user_service.get_by_id(user_id)
if not user:
raise ResourceNotFoundError(f"User {user_id} not found")
return {"user": user}
@app.put("/api/users/{user_id}")
async def update_user(
user_id: UUID,
update_data: dict,
current_user = deps.current_user
):
# Validate input
if "email" in update_data and not "@" in update_data["email"]:
raise ValidationError("Invalid email format")
# Update user
user = await deps.user_service.update_user(user_id, update_data)
return {"user": user}
Example 5: Template-Based Web App
from fastapi import Request
from fastapi.templating import Jinja2Templates
from portico.kits.fastapi import (
setup_template_globals,
create_template_context,
)
templates = Jinja2Templates(directory="templates")
setup_template_globals(templates, portico_app)
@app.get("/")
async def home(request: Request, user = deps.optional_user):
context = create_template_context(
request,
user=user,
page_title="Home"
)
return templates.TemplateResponse("home.html", context)
@app.get("/dashboard")
async def dashboard(request: Request, user = deps.current_user):
# Get user's data
groups = await deps.group_service.get_user_groups(user.id)
context = create_template_context(
request,
user=user,
groups=groups,
page_title="Dashboard"
)
return templates.TemplateResponse("dashboard.html", context)
Best Practices
1. Use Dependencies Class Instead of Dictionary
The Dependencies class provides type safety and IDE autocomplete:
# ✅ GOOD - Type-safe dependencies
from portico.kits.fastapi import Dependencies
deps = Dependencies(app)
@app.get("/users")
async def list_users(user = deps.current_user): # Autocomplete works
return {"user": user.email}
# ❌ BAD - Deprecated dictionary pattern (no type safety)
from portico.kits.fastapi import create_all_dependencies
dependencies = create_all_dependencies(app)
@app.get("/users")
async def list_users(user = dependencies["get_current_user"]): # No autocomplete
return {"user": user.email}
2. Register Exception Handlers Early
Register handlers before defining routes:
# ✅ GOOD - Register handlers at startup
from portico.kits.fastapi import register_exception_handlers
app = FastAPI()
register_exception_handlers(app) # Register first
@app.get("/users")
async def list_users():
# Exceptions automatically handled
pass
# ❌ BAD - Registering after routes might miss some handlers
@app.get("/users")
async def list_users():
pass
register_exception_handlers(app) # Too late
3. Use Decorators for Simple Auth Checks
Decorators are cleaner than manual checks:
# ✅ GOOD - Declarative with decorator
from portico.kits.fastapi import requires_permission
@app.delete("/users/{user_id}")
@requires_permission(app, "users.delete")
async def delete_user(user_id: UUID, current_user):
await user_service.delete_user(user_id)
return {"deleted": True}
# ❌ BAD - Manual permission check (verbose)
@app.delete("/users/{user_id}")
async def delete_user(user_id: UUID, user = deps.current_user):
has_perm = await deps.rbac_service.check_permission(
user.id, "users.delete"
)
if not has_perm:
raise HTTPException(403, "Permission denied")
await user_service.delete_user(user_id)
return {"deleted": True}
4. Add Request Logging Middleware
Enable structured logging for all requests:
# ✅ GOOD - Request logging enabled
from portico.kits.fastapi import RequestLoggingMiddleware
app = FastAPI()
app.add_middleware(RequestLoggingMiddleware)
# All requests automatically logged with request IDs
# ❌ BAD - No structured logging
app = FastAPI()
# Requests not logged, no request IDs
5. Use optional_user for Public/Private Content
Differentiate between public and auth-required routes:
# ✅ GOOD - Public route with optional auth
@app.get("/")
async def home(user = deps.optional_user):
if user:
return {"message": f"Welcome back, {user.email}"}
return {"message": "Welcome, guest"}
# ✅ GOOD - Auth required
@app.get("/dashboard")
async def dashboard(user = deps.current_user):
# Only authenticated users can access
return {"dashboard": "data"}
# ❌ BAD - Using current_user for public route
@app.get("/")
async def home(user = deps.current_user):
# Raises 401 for unauthenticated users!
return {"message": "Welcome"}
6. Leverage Template Globals
Make app features available in all templates:
# ✅ GOOD - Setup globals once
from portico.kits.fastapi import setup_template_globals
templates = Jinja2Templates(directory="templates")
setup_template_globals(templates, app)
# Now all templates can access app
# { { app.render_assets('head')|safe } }
# { { app.version } }
# ❌ BAD - Manually passing app to every route
@app.get("/page1")
async def page1(request: Request):
return templates.TemplateResponse("page1.html", {
"request": request,
"app": app # Repetitive
})
@app.get("/page2")
async def page2(request: Request):
return templates.TemplateResponse("page2.html", {
"request": request,
"app": app # Repetitive
})
7. Use create_template_context Helper
Simplify template context creation:
# ✅ GOOD - Helper function
from portico.kits.fastapi import create_template_context
@app.get("/dashboard")
async def dashboard(request: Request, user = deps.current_user):
context = create_template_context(
request,
user=user,
page_title="Dashboard"
)
return templates.TemplateResponse("dashboard.html", context)
# ❌ BAD - Manual dictionary construction
@app.get("/dashboard")
async def dashboard(request: Request, user = deps.current_user):
context = {
"request": request, # Must remember this
"user": user,
"page_title": "Dashboard"
}
return templates.TemplateResponse("dashboard.html", context)
Security Considerations
Authentication Token Storage
Use secure cookie settings for session tokens:
response.set_cookie(
key="access_token",
value=token,
httponly=True, # Prevent JavaScript access (XSS protection)
secure=True, # HTTPS only
samesite="lax", # CSRF protection
max_age=3600 # 1 hour expiration
)
Permission Checking
Always check permissions before performing sensitive operations:
# Check permission before action
@app.delete("/users/{user_id}")
@requires_permission(app, "users.delete")
async def delete_user(user_id: UUID, current_user):
# Permission already validated by decorator
await user_service.delete_user(user_id)
return {"deleted": True}
Input Validation
Validate user input before processing:
from pydantic import BaseModel, EmailStr, validator
class CreateUserRequest(BaseModel):
email: EmailStr
username: str
password: str
@validator('username')
def validate_username(cls, v):
if len(v) < 3:
raise ValueError('Username must be at least 3 characters')
return v
@app.post("/users")
async def create_user(user_data: CreateUserRequest):
# Pydantic validates input automatically
user = await deps.user_service.create_user(user_data)
return user
Rate Limiting
Implement rate limiting for sensitive endpoints:
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.post("/login")
@limiter.limit("5/minute")
async def login(request: Request, email: str, password: str):
# Limited to 5 attempts per minute per IP
result = await deps.auth_service.authenticate(email, password)
return {"token": result.token}
FAQs
Q: How do I access the current user in non-route functions?
A: Pass the user as a parameter from your route handler:
async def send_notification(user: User, message: str):
# Business logic that needs user context
pass
@app.post("/notify")
async def notify(message: str, user = deps.current_user):
await send_notification(user, message)
return {"sent": True}
Q: Can I use multiple decorators on the same route?
A: Yes, stack decorators in order (authentication first, then authorization):
@app.delete("/admin/users/{user_id}")
@requires_auth(app) # First: check authentication
@requires_role(app, "admin") # Second: check role
@requires_permission(app, "users.delete") # Third: check permission
async def delete_user(user_id: UUID, current_user):
await user_service.delete_user(user_id)
return {"deleted": True}
Q: How do I customize exception responses?
A: Create custom handlers or modify existing ones:
from portico.exceptions import ValidationError
async def custom_validation_handler(request, exc):
return JSONResponse(
status_code=400,
content={
"error": "validation_failed",
"message": str(exc),
"timestamp": datetime.now().isoformat()
}
)
app.add_exception_handler(ValidationError, custom_validation_handler)
Q: How do I use Bearer tokens instead of cookies?
A: The Dependencies class automatically checks both:
# Supports both:
# Cookie: access_token=<token>
# Header: Authorization: Bearer <token>
@app.get("/api/data")
async def get_data(user = deps.current_user):
# Works with either auth method
return {"data": "value"}
Q: Can I use the FastAPI Kit without AuthKit?
A: Yes, but authentication features (current_user, optional_user, auth decorators) won't work. Other features (exception handlers, middleware, template helpers) work independently.
Q: How do I test routes that use dependencies?
A: Override dependencies in tests:
from fastapi.testclient import TestClient
def test_protected_route():
# Create test user
test_user = User(id=uuid4(), email="test@example.com")
# Override dependency
app.dependency_overrides[deps.current_user] = lambda: test_user
client = TestClient(app)
response = client.get("/dashboard")
assert response.status_code == 200
assert "test@example.com" in response.text
Q: How do I add custom middleware?
A: Add it alongside the request logging middleware:
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Portico request logging
app.add_middleware(RequestLoggingMiddleware)
Q: Can I use the Dependencies class with API routes and HTML routes?
A: Yes, it works with both JSON APIs and template-based routes:
# JSON API
@app.get("/api/users")
async def api_users(user = deps.current_user):
return {"users": await user_service.list_users()}
# HTML template
@app.get("/users")
async def html_users(request: Request, user = deps.current_user):
context = create_template_context(request, user=user)
return templates.TemplateResponse("users.html", context)
Q: How do I handle different permission levels for the same resource?
A: Use multiple decorators or manual checks:
# Read permission
@app.get("/documents/{doc_id}")
@requires_permission(app, "documents.read")
async def read_document(doc_id: UUID, current_user):
return await document_service.get(doc_id)
# Write permission
@app.put("/documents/{doc_id}")
@requires_permission(app, "documents.write")
async def update_document(doc_id: UUID, data: dict, current_user):
return await document_service.update(doc_id, data)
# Delete permission (requires admin)
@app.delete("/documents/{doc_id}")
@requires_role(app, "admin")
async def delete_document(doc_id: UUID, current_user):
return await document_service.delete(doc_id)