HTTP Utilities
Standardized response formatters, type-safe exceptions, automatic error handling, and middleware — so every endpoint across every service returns the same consistent JSON structure.
success, error, paginated — same JSON shape everywhere.
404, 422, 401, 403 — raise and let the handler format them.
Register once — all errors auto-formatted consistently.
RequestID tracking, automatic locale detection.
Introduction
The HTTP module solves a deceptively simple problem: making sure every response from every endpoint in your application has the same JSON structure. Without it, different developers write {"user": ...}, {"data": ...}, {"result": ...} — and your frontend team deals with the inconsistency.
With FastKit HTTP utilities, every success response looks like this:
{
"success": true,
"data": { "id": 1, "name": "Alice" },
"message": "User retrieved successfully"
}
{
"success": false,
"message": "Validation failed",
"errors": {
"email": ["The email must be a valid email address"],
"password": ["Password must be at least 8 characters"]
}
}
{
"success": true,
"data": [
{ "id": 1, "name": "Product A" },
{ "id": 2, "name": "Product B" }
],
"pagination": {
"page": 1, "per_page": 20,
"total": 100, "total_pages": 5,
"has_next": true, "has_prev": false
},
"message": "Products retrieved"
}
Quick Example
A minimal setup that covers all the essentials:
from fastapi import FastAPI, Depends
from fastkit_core.http import (
success_response, paginated_response,
NotFoundException,
register_exception_handlers,
RequestIDMiddleware, LocaleMiddleware,
get_pagination
)
app = FastAPI()
# 1. Register exception handlers (before routes)
register_exception_handlers(app)
# 2. Add middleware
app.add_middleware(LocaleMiddleware)
app.add_middleware(RequestIDMiddleware)
# 3. Use in routes
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = service.find(user_id)
if not user:
raise NotFoundException("User not found") # auto-formatted
return success_response(data=user.to_dict())
@app.get("/users")
def list_users(pagination: dict = Depends(get_pagination)):
users, meta = service.paginate(**pagination)
return paginated_response(items=[u.to_dict() for u in users], pagination=meta)
Response Formatters
Use for any successful operation — GET, POST, PUT, PATCH:
from fastkit_core.http import success_response
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = service.find_or_fail(user_id)
return success_response(data=user.to_dict())
@app.post("/users", status_code=201)
def create_user(data: UserCreate):
user = service.create(data)
return success_response(
data=user.model_dump(),
message="User created successfully",
status_code=201
)
Any — optional. The response payload.str — optional. Human-readable success message.int — default 200. HTTP status code.For manually-constructed error responses. For most cases, raising an exception is cleaner:
from fastkit_core.http import error_response
return error_response(
message="Validation failed",
errors={"email": ["Invalid email format"]},
status_code=422
)
str — required. Error description.dict — optional. Field-level error details.int — default 400. HTTP status code.For list endpoints with pagination metadata from the repository:
from fastkit_core.http import paginated_response, get_pagination
@app.get("/products")
def list_products(pagination: dict = Depends(get_pagination)):
products, meta = service.paginate(**pagination)
return paginated_response(
items=[p.to_dict() for p in products],
pagination=meta,
message="Products retrieved successfully"
)
list — required. The records for this page.dict — required. Meta from repo.paginate(): page, per_page, total, total_pages, has_next, has_prev.str — optional.int — default 200.Exceptions
Raise exceptions instead of manually building error responses — the global exception handler formats them automatically.
Built-in exceptions
| Exception | Status | Default message | Use for |
|---|---|---|---|
NotFoundException | 404 | Resource not found | Record doesn't exist |
ValidationException | 422 | Validation failed | Business rule violations |
UnauthorizedException | 401 | Unauthorized | Missing/invalid token |
ForbiddenException | 403 | Forbidden | Insufficient permissions |
FastKitException | any | — | Base class / custom codes |
from fastkit_core.http import (
NotFoundException,
ValidationException,
UnauthorizedException,
ForbiddenException,
FastKitException,
)
# 404 — record missing
raise NotFoundException("User not found")
# 422 — business rule
raise ValidationException(
errors={"email": ["Email already registered"]},
message="Validation failed"
)
# 401 — auth failure
raise UnauthorizedException("Token expired")
# 403 — permission
raise ForbiddenException("Admin access required")
# Custom status code
raise FastKitException("Payment required", status_code=402)
# With field errors on any exception
raise FastKitException(
message="Operation failed",
status_code=400,
errors={"balance": ["Insufficient funds"]}
)
Custom exception
from fastkit_core.http import FastKitException
class PaymentRequiredException(FastKitException):
def __init__(self, message: str = "Payment required"):
super().__init__(message, status_code=402)
class RateLimitException(FastKitException):
def __init__(self, retry_after: int = 60):
super().__init__(
f"Too many requests. Retry after {retry_after}s",
status_code=429
)
# Usage
raise PaymentRequiredException("Subscribe to access this feature")
raise RateLimitException(retry_after=30)
Exception Handlers
Call register_exception_handlers(app) once — before defining any routes — and every exception in your application is automatically formatted consistently.
from fastapi import FastAPI
from fastkit_core.http import register_exception_handlers
app = FastAPI()
register_exception_handlers(app) # ← register BEFORE routes
@app.get("/users/{user_id}")
def get_user(user_id: int): ...
What it handles
{"success": false, "message": "...", "errors": {}}Debug vs Production
# config/app.py
DEBUG = True # development — shows actual error message
DEBUG = False # production — hides details, returns "Internal server error"
Middleware
Generates a unique UUID for each request, available via request.state.request_id and added to the response as X-Request-ID header.
from fastapi import FastAPI, Request
from fastkit_core.http import RequestIDMiddleware
app = FastAPI()
app.add_middleware(RequestIDMiddleware)
@app.get("/")
def root(request: Request):
request_id = request.state.request_id
logger.info(f"[{request_id}] Processing request")
return {"request_id": request_id}
# Response header automatically added:
# X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
In microservice architectures, propagate X-Request-ID between services — it lets you trace a single user request across all services in your logs.
Automatically detects user locale and sets it for the request. Used by validation and translation systems to return messages in the user's language.
from fastkit_core.http import LocaleMiddleware
app.add_middleware(LocaleMiddleware)
Accept-Language header
→ es-ES becomes es
?lang= query parameter
→ ?lang=fr
locale cookie
→ locale=de
en
Create your own middleware following the same pattern:
import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
start = time.time()
response = await call_next(request)
response.headers['X-Process-Time'] = f"{time.time() - start:.4f}s"
return response
app.add_middleware(TimingMiddleware)
Middleware executes in reverse registration order — last added runs first:
app.add_middleware(LocaleMiddleware) # runs 3rd
app.add_middleware(RequestIDMiddleware) # runs 2nd
app.add_middleware(TimingMiddleware) # runs 1st (outermost)
# Request flow:
# TimingMiddleware → RequestIDMiddleware → LocaleMiddleware → route handler
# Response flow:
# route handler → LocaleMiddleware → RequestIDMiddleware → TimingMiddleware
Add RequestIDMiddleware last (runs first) so the request ID is available to all other middleware for logging.
Dependencies
Reusable FastAPI dependencies that handle common parameter extraction.
get_pagination
Extracts and validates page and per_page from query parameters:
from fastapi import Depends
from fastkit_core.http import get_pagination, paginated_response
@app.get("/products")
def list_products(pagination: dict = Depends(get_pagination)):
# ?page=2&per_page=50
# pagination = {'page': 2, 'per_page': 50, 'offset': 50}
products, meta = service.paginate(
page=pagination['page'],
per_page=pagination['per_page']
)
return paginated_response(items=[p.to_dict() for p in products], pagination=meta)
| Parameter | Default | Constraints |
|---|---|---|
page | 1 | min 1 |
per_page | 20 | min 1, max 100 |
get_locale
from fastkit_core.http import get_locale as locale_dep
from fastkit_core.i18n import _
@app.get("/greeting")
def greeting(locale: str = Depends(locale_dep)):
return {
"locale": locale,
"message": _('messages.hello')
}
# GET /greeting?locale=es → locale = "es"
# GET /greeting → locale from context (set by LocaleMiddleware)
API Reference
Response Formatters
JSONResponse with {"success": true, "data": ..., "message": ...}. All params optional.JSONResponse with {"success": false, "message": ..., "errors": ...}. message required.JSONResponse with {"success": true, "data": [...], "pagination": {...}}.Exceptions
status_code defaults to 400. All FastKit exceptions inherit from this.errors is dict[str, list[str]]. Default message: "Validation failed".Setup functions
Middleware
request.state.request_id and X-Request-ID response header.Dependencies
page (default 1) and per_page (default 20, max 100). Returns {'page', 'per_page', 'offset'}.?locale= param or context.Complete Example
A full FastAPI application with all HTTP utilities configured:
from fastapi import FastAPI, Depends
from fastkit_core.http import (
success_response, paginated_response,
NotFoundException, ValidationException, ForbiddenException,
register_exception_handlers,
RequestIDMiddleware, LocaleMiddleware,
get_pagination,
)
from fastkit_core.i18n import _
app = FastAPI(title="My FastKit API")
# ── Setup (order matters) ─────────────────────────
register_exception_handlers(app) # exception handling
app.add_middleware(LocaleMiddleware) # locale detection
app.add_middleware(RequestIDMiddleware) # request ID (runs first)
# ── Routes ────────────────────────────────────────
@app.get("/")
def root():
return success_response(
data={"version": "1.0.0"},
message=_('messages.welcome')
)
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = service.find(user_id)
if not user:
raise NotFoundException(_('errors.user_not_found'))
return success_response(data=user.to_dict())
@app.get("/users")
def list_users(pagination: dict = Depends(get_pagination)):
users, meta = service.paginate(**pagination)
return paginated_response(
items=[u.to_dict() for u in users],
pagination=meta
)
@app.post("/users", status_code=201)
def create_user(data: UserCreate, current_user = Depends(get_current_user)):
if not current_user.can_create_users:
raise ForbiddenException("Insufficient permissions")
# Service validation errors bubble up automatically
user = service.create(data)
return success_response(
data=user.model_dump(),
message=_('messages.user_created'),
status_code=201
)
@app.delete("/users/{user_id}", status_code=204)
def delete_user(user_id: int, current_user = Depends(get_current_user)):
if not current_user.is_admin:
raise ForbiddenException("Admin access required")
service.delete(user_id)