Docs / fastkit-core / HTTP Utilities

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.

Response Formatters

success, error, paginated — same JSON shape everywhere.

Custom Exceptions

404, 422, 401, 403 — raise and let the handler format them.

Global Handlers

Register once — all errors auto-formatted consistently.

Middleware

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
Error
Paginated
json
{
  "success": true,
  "data": { "id": 1, "name": "Alice" },
  "message": "User retrieved successfully"
}
json
{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "email":    ["The email must be a valid email address"],
    "password": ["Password must be at least 8 characters"]
  }
}
json
{
  "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:

python
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:

python
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
    )
data
Any — optional. The response payload.
message
str — optional. Human-readable success message.
status_code
int — default 200. HTTP status code.

For manually-constructed error responses. For most cases, raising an exception is cleaner:

python
from fastkit_core.http import error_response

return error_response(
    message="Validation failed",
    errors={"email": ["Invalid email format"]},
    status_code=422
)
message
str — required. Error description.
errors
dict — optional. Field-level error details.
status_code
int — default 400. HTTP status code.

For list endpoints with pagination metadata from the repository:

python
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"
    )
items
list — required. The records for this page.
pagination
dict — required. Meta from repo.paginate(): page, per_page, total, total_pages, has_next, has_prev.
message
str — optional.
status_code
int — default 200.

Exceptions

Raise exceptions instead of manually building error responses — the global exception handler formats them automatically.

Built-in exceptions

ExceptionStatusDefault messageUse for
NotFoundException404Resource not foundRecord doesn't exist
ValidationException422Validation failedBusiness rule violations
UnauthorizedException401UnauthorizedMissing/invalid token
ForbiddenException403ForbiddenInsufficient permissions
FastKitExceptionanyBase class / custom codes
python
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

python
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.

python
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

FastKitException
All FastKit exceptions (404, 401, 403, 422, custom) — formatted to {"success": false, "message": "...", "errors": {}}
RequestValidationError
FastAPI's 422 validation errors — converted to FastKit's structured format with field-level messages
ValidationError
Pydantic validation errors with automatic translation to the current locale
Exception
Unexpected exceptions — detailed message in DEBUG mode, generic "Internal server error" in production

Debug vs Production

python
# 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.

python
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.

python
from fastkit_core.http import LocaleMiddleware
app.add_middleware(LocaleMiddleware)
1 Accept-Language header es-ES becomes es
2 ?lang= query parameter ?lang=fr
3 locale cookie locale=de
4 Default en

Create your own middleware following the same pattern:

python
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:

python
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:

python
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)
ParameterDefaultConstraints
page1min 1
per_page20min 1, max 100

get_locale

python
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

success_response(data, message, status_code)
Returns JSONResponse with {"success": true, "data": ..., "message": ...}. All params optional.
error_response(message, errors, status_code)
Returns JSONResponse with {"success": false, "message": ..., "errors": ...}. message required.
paginated_response(items, pagination, message, status_code)
Returns JSONResponse with {"success": true, "data": [...], "pagination": {...}}.

Exceptions

FastKitException(message, status_code, errors)
Base class. status_code defaults to 400. All FastKit exceptions inherit from this.
NotFoundException(message)
HTTP 404. Default message: "Resource not found".
ValidationException(errors, message)
HTTP 422. errors is dict[str, list[str]]. Default message: "Validation failed".
UnauthorizedException(message)
HTTP 401. Default message: "Unauthorized".
ForbiddenException(message)
HTTP 403. Default message: "Forbidden".

Setup functions

register_exception_handlers(app)
Registers handlers for FastKitException, RequestValidationError, ValidationError, and generic Exception. Call before defining routes.

Middleware

RequestIDMiddleware
Adds UUID to request.state.request_id and X-Request-ID response header.
LocaleMiddleware
Sets locale from Accept-Language → ?lang= → cookie → default "en".

Dependencies

get_pagination
Extracts page (default 1) and per_page (default 20, max 100). Returns {'page', 'per_page', 'offset'}.
get_locale
Returns current locale from ?locale= param or context.

Complete Example

A full FastAPI application with all HTTP utilities configured:

python
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)