Next.js-style file-based routing for FastAPI
Define your API routes through directory structure and convention, not manual registration. Create a route.py file in a directory and it becomes an endpoint automatically. Say goodbye to router boilerplate and route registration conflicts.
- 🚀 Installation & Quickstart
- 📁 File-Based Routing Explained
- 📍 Route Handlers
- 🔗 Middleware
- 📦 Examples
- 📖 API Reference
- 💡 Why This Plugin?
- 🤝 Contributing
Requires Python 3.10+ and FastAPI 0.65.0+.
pip install fastapi-filebased-routingfrom fastapi import FastAPI
from fastapi_filebased_routing import create_router_from_path
app = FastAPI()
app.include_router(create_router_from_path("app"))That's it. Every route.py file in your app/ directory is automatically discovered and registered.
Your directory structure defines your URL routes. Given create_router_from_path("app"):
app/
├── _middleware.py # directory middleware → ALL routes
├── health/
│ └── route.py # get → /health
├── api/
│ ├── _middleware.py # directory middleware → /api/**
│ ├── [[version]]/
│ │ └── route.py # → /api and /api/{version}
│ └── users/
│ ├── route.py # file-level middleware + handlers
│ └── [user_id]/
│ └── route.py # handler-level middleware via class
├── files/
│ └── [...path]/
│ └── route.py # catch-all route
├── ws/
│ └── chat/
│ └── route.py # websocket handler
└── (admin)/ # group: excluded from URL
├── _middleware.py # directory middleware → /settings/**
└── settings/
└── route.py # → /settings
Each route.py exports route handlers. Each _middleware.py defines directory middleware.
| Convention | Route Example | URL | Handler Parameter |
|---|---|---|---|
users/ |
app/users/route.py |
/users |
— |
[id]/ |
app/users/[id]/route.py |
/users/123 |
id: str |
[[version]]/ |
app/api/[[version]]/route.py |
/api and /api/v2 |
version: str | None |
[...path]/ |
app/files/[...path]/route.py |
/files/a/b/c |
path: str |
(group)/ |
app/(admin)/settings/route.py |
/settings |
— |
Files: route.py contains handlers. _middleware.py contains directory middleware that cascades to all subdirectories.
Each route.py exports handlers. Supported HTTP methods: get, post, put, patch, delete, head, options, websocket. Functions prefixed with _ are private helpers and ignored. Default status codes: POST → 201, DELETE → 204, all others → 200.
# app/api/users/route.py
from fastapi_filebased_routing import route
# Module-level metadata (applies to all handlers in this file)
TAGS = ["users"] # auto-derived from first path segment if omitted
SUMMARY = "User management endpoints" # OpenAPI summary
DEPRECATED = True # mark all handlers as deprecated
# File-level middleware (applies to all handlers in this file, does NOT cascade to subdirectories)
middleware = [rate_limit(100)]
# Simple handler — just a function
async def get():
"""List users."""
return {"users": []}
# Configured handler — per-handler control over metadata and middleware
class post(route):
status_code = 200 # override convention-based 201
tags = ["admin"] # override module-level TAGS
summary = "Create a user" # override module-level SUMMARY
deprecated = True # override module-level DEPRECATED
middleware = [require_role("admin")] # or use inline: async def middleware(request, call_next): ...
async def handler(name: str):
return {"name": name}Both styles coexist freely. Directory bracket names (e.g., [user_id]) become path parameters automatically injected into handler signatures. See examples/ for complete working projects.
Three-layer middleware system that lets you scope cross-cutting concerns (auth, logging, rate limiting) to directories, files, or individual handlers. Middleware is validated at startup and assembled into chains with zero per-request overhead.
All middleware functions use the same signature:
async def my_middleware(request, call_next):
# Before handler
response = await call_next(request)
# After handler
return responseMiddleware must be async. Sync middleware raises a validation error at startup.
Already have middleware written as classes (e.g. Starlette's BaseHTTPMiddleware)? Use dispatch() to adapt them without rewriting as functions:
from fastapi_filebased_routing import dispatch
middleware = [
log_request, # function middleware
dispatch(RateLimiterMiddleware, limit=100), # class-based middleware
]Any class that accepts app as its first constructor argument and exposes an async dispatch(request, call_next) method works — no BaseHTTPMiddleware import required. The class is lazily instantiated on the first request. dispatch() works at all three middleware layers below.
Create a _middleware.py file in any directory. Its middleware applies to all routes in that directory and all subdirectories. Use one of two forms:
List form — multiple middleware functions:
# app/api/_middleware.py — applies to all routes under /api/**
from app.auth import auth_required
from app.logging import request_logger
middleware = [auth_required, request_logger]Single function form — one inline middleware:
# app/_middleware.py — root-level timing middleware
import time
async def middleware(request, call_next):
start = time.monotonic()
response = await call_next(request)
response.headers["X-Response-Time"] = f"{time.monotonic() - start:.4f}"
return responsePick one form per file. If both are defined, standard Python name resolution applies — the last assignment to middleware wins.
Directory middleware cascades: a _middleware.py in app/ applies to every route, while one in app/api/ only applies to routes under /api/. Parent middleware always runs before child middleware.
Set middleware = [...] at the top of any route.py to apply middleware to all handlers in that file. Unlike directory middleware, file-level middleware does not cascade to subdirectories.
# app/api/users/route.py
middleware = [rate_limit(100)] # applies to get and post below, not to /api/users/[user_id]
async def get():
"""List users. Rate limited."""
return {"users": []}
async def post(name: str):
"""Create user. Rate limited."""
return {"name": name}class handler(route): blocks support a middleware attribute — as a list or a single inline function:
# app/api/orders/route.py
from fastapi_filebased_routing import route
class post(route):
async def middleware(request, call_next):
if not request.headers.get("X-Idempotency-Key"):
from fastapi.responses import JSONResponse
return JSONResponse(
{"error": "X-Idempotency-Key header required"},
status_code=400,
)
return await call_next(request)
async def handler(order: dict):
"""Create order. Requires idempotency key."""
return {"order_id": "abc-123", **order}When a request hits a route with middleware at multiple levels, they execute in this order:
Directory middleware (root → leaf)
→ File-level middleware
→ Handler-level middleware
→ Handler function
← Handler-level middleware
← File-level middleware
← Directory middleware
Each middleware can modify the request before calling call_next, and modify the response after. Middleware can also short-circuit by returning a response without calling call_next:
async def auth_guard(request, call_next):
if not request.headers.get("Authorization"):
return JSONResponse({"error": "unauthorized"}, status_code=401)
return await call_next(request)See the examples/ directory for runnable projects:
basic/— Routing fundamentals: static, dynamic, CRUDmiddleware/— All three middleware layers in actionadvanced/— Optional params, catch-all, route groups, WebSockets
def create_router_from_path(
base_path: str | Path,
*,
prefix: str = "",
) -> APIRouterCreate a FastAPI APIRouter from a directory of route.py files.
Parameters:
base_path(str | Path): Root directory containing route.py filesprefix(str, optional): URL prefix for all discovered routes (default: "")
Returns:
APIRouter: A FastAPI router with all discovered routes registered
Raises:
RouteDiscoveryError: If base_path doesn't exist or isn't a directoryRouteValidationError: If a route file has invalid exports or parametersDuplicateRouteError: If two route files resolve to the same path+methodPathParseError: If a directory name has invalid syntaxMiddlewareValidationError: If a_middleware.pyfile fails to import or contains invalid middleware
Example:
from fastapi import FastAPI
from fastapi_filebased_routing import create_router_from_path
app = FastAPI()
# Basic usage
app.include_router(create_router_from_path("app"))
# With prefix
app.include_router(create_router_from_path("app", prefix="/api/v1"))
# Multiple routers
app.include_router(create_router_from_path("app/public"))
app.include_router(create_router_from_path("app/admin", prefix="/admin"))Base class for handler-level middleware and metadata configuration. Uses a metaclass that returns a RouteConfig instead of a class.
from fastapi_filebased_routing import route
class get(route):
middleware = [auth_required]
tags = ["users"]
summary = "Get user details"
async def handler(user_id: str):
return {"user_id": user_id}
# `get` is now a RouteConfig, not a class
# `get(user_id="123")` calls the handler directlydef dispatch(cls: type, **kwargs) -> CallableAdapt a class-based middleware for use in _middleware.py or any middleware list.
Parameters:
cls(type): Middleware class. Must acceptappas its first constructor argument and expose an asyncdispatch(request, call_next)method.**kwargs: Arguments forwarded tocls.__init__(afterapp).
Returns:
- An async middleware function compatible with the middleware pipeline.
Example:
from fastapi_filebased_routing import dispatch
# No kwargs
middleware = [dispatch(LoggingMiddleware)]
# With kwargs
middleware = [dispatch(RateLimiterMiddleware, limit=100, burst=20)]FastAPI developers building medium-to-large APIs face these problems:
- Manual route registration is tedious. Every endpoint requires updating a centralized router file.
- Route discoverability degrades. Finding the handler for
/api/v1/users/{id}requires searching across files. - Middleware wiring is repetitive. Applying auth to 20 admin endpoints means 20 copies of
Depends(require_admin). - Full-stack developers experience friction. Next.js has file-based routing, FastAPI requires manual wiring.
This plugin solves all four with:
- Zero-configuration route discovery from directory structure
- Three-layer middleware system (directory, file, handler) with cascading inheritance
- Next.js feature parity: dynamic params, optional params, catch-all, route groups
- Convention over configuration for status codes, tags, and metadata
- Battle-tested security (path traversal protection, symlink validation)
- WebSocket support, sync and async handlers
- Hot reload compatible (
uvicorn --reloadworks out of the box) - Full mypy strict mode support, coexists with manual FastAPI routing
This plugin is extracted from a production codebase and is actively maintained. Issues, feature requests, and pull requests are welcome.
GitHub: https://github.com/rsmdt/fastapi-filebased-routing.py