fastapi-filebased-routing
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.
Table of Contents
- 🚀 Installation & Quickstart
- 📁 File-Based Routing Explained
- 📍 Route Handlers
- 🔗 Middleware
- 📦 Examples
- 📖 API Reference
- 💡 Why This Plugin?
- 🤝 Contributing
🚀 Installation & Quickstart
Requires Python 3.10+ and FastAPI 0.65.0+.
pip install fastapi-filebased-routing
from 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.
📁 File-Based Routing Explained
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.
Route Conventions
| 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.
📍 Route Handlers
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.
🔗 Middleware
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 response
Middleware 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.
Directory-Level Middleware
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 response
Pick 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.
File-Level 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}
Handler-Level Middleware
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}
Execution 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)
📦 Examples
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
📖 API Reference
create_router_from_path
def create_router_from_path(
base_path: str | Path,
*,
prefix: str = "",
) -> APIRouter
Create 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"))
route
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 directly
dispatch
def dispatch(cls: type, **kwargs) -> Callable
Adapt 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)]
💡 Why This Plugin?
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
🤝 Contributing
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
Release files for fastapi-filebased-routing 1.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastapi_filebased_routing-1.2.1.tar.gz | 23.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_filebased_routing-1.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 51.7 kB
Release files / fastapi_filebased_routing-1.2.1.tar.gz
| Download URL | fastapi_filebased_routing-1.2.1.tar.gz |
|---|---|
| Size | 23.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7481c60fa2ac73e52afbb690d69492b58b1271074d8a4f6538d7edfbd61935ae
|
|
BLAKE2b-256 checksum How to use checksums |
8e569ac1886042c6a3d7e42d547b174c667d5f56df2db0755c567eca370599ba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.10 {"installer":{"name":"uv","version":"0.11.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / fastapi_filebased_routing-1.2.1-py3-none-any.whl
| Download URL | fastapi_filebased_routing-1.2.1-py3-none-any.whl |
|---|---|
| Size | 28.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6dbf7af131395d9b7c055565ef1715ec5616db515c3d55d6b2b082ecfdd9fbb6
|
|
BLAKE2b-256 checksum How to use checksums |
7efa0ba7526b9667f9fa10422ece5d93a2cb704b558b243f1c42e1111f6f0c30
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.10 {"installer":{"name":"uv","version":"0.11.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|