fastapi-filebased-routing 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. fastapi_filebased_routing-1.0.0/.github/workflows/ci.yml +50 -0
  2. fastapi_filebased_routing-1.0.0/.github/workflows/publish.yml +31 -0
  3. fastapi_filebased_routing-1.0.0/.gitignore +29 -0
  4. fastapi_filebased_routing-1.0.0/.pre-commit-config.yaml +24 -0
  5. fastapi_filebased_routing-1.0.0/CHANGELOG.md +61 -0
  6. fastapi_filebased_routing-1.0.0/LICENSE +21 -0
  7. fastapi_filebased_routing-1.0.0/PKG-INFO +359 -0
  8. fastapi_filebased_routing-1.0.0/README.md +326 -0
  9. fastapi_filebased_routing-1.0.0/examples/advanced/app/(admin)/settings/route.py +40 -0
  10. fastapi_filebased_routing-1.0.0/examples/advanced/app/api/[[version]]/users/route.py +43 -0
  11. fastapi_filebased_routing-1.0.0/examples/advanced/app/events/route.py +64 -0
  12. fastapi_filebased_routing-1.0.0/examples/advanced/app/files/[...path]/route.py +45 -0
  13. fastapi_filebased_routing-1.0.0/examples/advanced/app/ws/chat/[room_id]/route.py +49 -0
  14. fastapi_filebased_routing-1.0.0/examples/advanced/main.py +58 -0
  15. fastapi_filebased_routing-1.0.0/examples/basic/app/health/route.py +9 -0
  16. fastapi_filebased_routing-1.0.0/examples/basic/app/users/[user_id]/route.py +39 -0
  17. fastapi_filebased_routing-1.0.0/examples/basic/app/users/route.py +42 -0
  18. fastapi_filebased_routing-1.0.0/examples/basic/main.py +24 -0
  19. fastapi_filebased_routing-1.0.0/examples/middleware/app/(admin)/_middleware.py +10 -0
  20. fastapi_filebased_routing-1.0.0/examples/middleware/app/(admin)/settings/route.py +8 -0
  21. fastapi_filebased_routing-1.0.0/examples/middleware/app/_middleware.py +9 -0
  22. fastapi_filebased_routing-1.0.0/examples/middleware/app/api/_middleware.py +11 -0
  23. fastapi_filebased_routing-1.0.0/examples/middleware/app/api/users/[user_id]/route.py +14 -0
  24. fastapi_filebased_routing-1.0.0/examples/middleware/app/api/users/route.py +15 -0
  25. fastapi_filebased_routing-1.0.0/examples/middleware/app/health/route.py +3 -0
  26. fastapi_filebased_routing-1.0.0/examples/middleware/main.py +9 -0
  27. fastapi_filebased_routing-1.0.0/pyproject.toml +95 -0
  28. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/__init__.py +44 -0
  29. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/__init__.py +5 -0
  30. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/importer.py +420 -0
  31. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/middleware.py +222 -0
  32. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/parser.py +197 -0
  33. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/scanner.py +283 -0
  34. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/exceptions.py +91 -0
  35. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/fastapi/__init__.py +5 -0
  36. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/fastapi/router.py +496 -0
  37. fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/py.typed +0 -0
  38. fastapi_filebased_routing-1.0.0/tests/conftest.py +132 -0
  39. fastapi_filebased_routing-1.0.0/tests/integration/__init__.py +1 -0
  40. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/__init__.py +0 -0
  41. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/conftest.py +58 -0
  42. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/main.py +27 -0
  43. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/_middleware.py +11 -0
  44. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/_middleware.py +6 -0
  45. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/items/[item_id]/route.py +26 -0
  46. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/messages/route.py +35 -0
  47. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/protected/_middleware.py +19 -0
  48. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/protected/route.py +19 -0
  49. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/tasks/[task_id]/route.py +28 -0
  50. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/users/route.py +23 -0
  51. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/echo/route.py +15 -0
  52. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/test_auth_isolation.py +124 -0
  53. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/test_body_isolation.py +54 -0
  54. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/test_error_isolation.py +118 -0
  55. fastapi_filebased_routing-1.0.0/tests/integration/concurrency/test_request_isolation.py +173 -0
  56. fastapi_filebased_routing-1.0.0/tests/integration/test_error_scenarios.py +730 -0
  57. fastapi_filebased_routing-1.0.0/tests/integration/test_full_routing.py +1303 -0
  58. fastapi_filebased_routing-1.0.0/tests/integration/test_middleware_errors.py +617 -0
  59. fastapi_filebased_routing-1.0.0/tests/integration/test_middleware_integration.py +907 -0
  60. fastapi_filebased_routing-1.0.0/tests/integration/test_security.py +621 -0
  61. fastapi_filebased_routing-1.0.0/tests/test_conftest_fixtures.py +109 -0
  62. fastapi_filebased_routing-1.0.0/tests/unit/__init__.py +1 -0
  63. fastapi_filebased_routing-1.0.0/tests/unit/core/__init__.py +1 -0
  64. fastapi_filebased_routing-1.0.0/tests/unit/core/test_importer.py +1111 -0
  65. fastapi_filebased_routing-1.0.0/tests/unit/core/test_middleware.py +454 -0
  66. fastapi_filebased_routing-1.0.0/tests/unit/core/test_middleware_chain.py +335 -0
  67. fastapi_filebased_routing-1.0.0/tests/unit/core/test_parser.py +331 -0
  68. fastapi_filebased_routing-1.0.0/tests/unit/core/test_scanner.py +532 -0
  69. fastapi_filebased_routing-1.0.0/tests/unit/core/test_smoke.py +24 -0
  70. fastapi_filebased_routing-1.0.0/tests/unit/fastapi/__init__.py +1 -0
  71. fastapi_filebased_routing-1.0.0/tests/unit/fastapi/test_router.py +1430 -0
  72. fastapi_filebased_routing-1.0.0/tests/unit/test_exceptions.py +295 -0
  73. fastapi_filebased_routing-1.0.0/tests/unit/test_public_api.py +149 -0
  74. fastapi_filebased_routing-1.0.0/uv.lock +616 -0
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ["3.13", "3.14"]
14
+ name: Test (Python ${{ matrix.python }})
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: astral-sh/setup-uv@v5
19
+
20
+ - name: Install Python
21
+ run: uv python install ${{ matrix.python }}
22
+
23
+ - name: Install dependencies
24
+ run: uv sync --extra dev
25
+
26
+ - name: Run tests
27
+ run: uv run pytest --tb=short -q
28
+
29
+ lint:
30
+ runs-on: ubuntu-latest
31
+ name: Lint & Types
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - uses: astral-sh/setup-uv@v5
36
+
37
+ - name: Install Python
38
+ run: uv python install 3.13
39
+
40
+ - name: Install dependencies
41
+ run: uv sync --extra dev
42
+
43
+ - name: Ruff check
44
+ run: uv run ruff check src/ tests/
45
+
46
+ - name: Ruff format
47
+ run: uv run ruff format --check src/ tests/
48
+
49
+ - name: Mypy
50
+ run: uv run mypy src/
@@ -0,0 +1,31 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ name: Build & Publish
11
+ permissions:
12
+ id-token: write
13
+ contents: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: astral-sh/setup-uv@v5
18
+
19
+ - name: Install Python
20
+ run: uv python install 3.13
21
+
22
+ - name: Build package
23
+ run: uv build
24
+
25
+ - name: Publish to PyPI
26
+ run: uv publish --trusted-publishing always
27
+
28
+ - name: Create GitHub Release
29
+ run: gh release create ${{ github.ref_name }} dist/* --generate-notes
30
+ env:
31
+ GH_TOKEN: ${{ github.token }}
@@ -0,0 +1,29 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+ *.egg-info/
6
+ *.egg
7
+ dist/
8
+ build/
9
+
10
+ # Virtual environments
11
+ .venv/
12
+
13
+ # Testing
14
+ .pytest_cache/
15
+ .coverage
16
+ htmlcov/
17
+
18
+ # Type checking
19
+ .mypy_cache/
20
+
21
+ # Ruff
22
+ .ruff_cache/
23
+
24
+ # IDE
25
+ .vscode/
26
+ .idea/
27
+
28
+ # OS
29
+ .DS_Store
@@ -0,0 +1,24 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: ruff-check
5
+ name: ruff check
6
+ entry: uv run ruff check --fix
7
+ language: system
8
+ types: [python]
9
+ files: ^(src|tests)/
10
+
11
+ - id: ruff-format
12
+ name: ruff format
13
+ entry: uv run ruff format
14
+ language: system
15
+ types: [python]
16
+ files: ^(src|tests)/
17
+
18
+ - id: mypy
19
+ name: mypy
20
+ entry: uv run mypy src/
21
+ language: system
22
+ types: [python]
23
+ files: ^src/
24
+ pass_filenames: false
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-02-08
9
+
10
+ ### Added
11
+
12
+ - **Directory-level middleware** via `_middleware.py` files — applies to all routes in the directory subtree
13
+ - **File-level middleware** via `middleware = [...]` at module level in `route.py` — applies to all handlers in the file
14
+ - **Handler-level middleware** via `class handler(route):` syntax with `middleware = [...]` — per-handler configuration
15
+ - **Three-layer middleware execution order**: directory (parent→child) → file → handler
16
+ - **`route` base class** with metaclass that returns `RouteConfig` — `class get(route):` returns callable config, not a class
17
+ - **`RouteConfig` dataclass** — carries handler, middleware, and metadata (tags, summary, deprecated, status_code)
18
+ - **`build_middleware_chain()`** — composable middleware chain with `call_next` semantics
19
+ - **`_make_middleware_route()`** — custom APIRoute subclass preserving FastAPI dependency injection
20
+ - **`MiddlewareValidationError`** exception for middleware configuration issues
21
+ - **Middleware context enrichment** — middleware can set `request.state` values visible to subsequent middleware and handlers
22
+ - **Middleware short-circuit** — middleware can return response without calling `call_next`
23
+ - **Startup-time validation** — all middleware validated when `create_router_from_path()` is called
24
+ - **Handler-level metadata override** — `class handler(route):` can set `tags`, `summary`, `deprecated`, `status_code`
25
+ - `examples/middleware/` — comprehensive example showing all middleware features
26
+
27
+ ### Changed
28
+
29
+ - Bumped version to 0.2.0
30
+
31
+ ### Fixed
32
+
33
+ - N/A
34
+
35
+ ## [0.1.0] - 2025-10-01
36
+
37
+ ### Added
38
+
39
+ - Automatic route discovery from directory structure with `create_router_from_path()`
40
+ - HTTP method handlers: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
41
+ - WebSocket handler support via `async def websocket(ws: WebSocket)` convention
42
+ - Dynamic path parameters using `[param]` directory syntax
43
+ - Optional path parameters using `[[param]]` directory syntax with automatic variant generation
44
+ - Catch-all path parameters using `[...param]` directory syntax for arbitrary nested paths
45
+ - Route groups using `(group)` directory syntax for code organization without URL impact
46
+ - Convention-based status codes (POST→201, DELETE→204, others→200)
47
+ - Route metadata support: TAGS, SUMMARY, DEPRECATED constants in route files
48
+ - Automatic tag derivation from path segments (skipping api/version prefixes)
49
+ - Security validation for path traversal attempts
50
+ - Symlink validation to prevent directory traversal attacks
51
+ - Parameter name validation (must be valid Python identifiers)
52
+ - Duplicate route detection with detailed error reporting
53
+ - Sync and async handler support (both `def` and `async def`)
54
+ - Full FastAPI APIRouter integration for seamless coexistence with manual routes
55
+ - Router prefix support via `prefix` parameter
56
+ - Type-safe with PEP 561 marker
57
+ - 98%+ test coverage across all modules
58
+
59
+ [Unreleased]: https://github.com/irudi/fastapi-filebased-routing/compare/v0.2.0...main
60
+ [0.2.0]: https://github.com/irudi/fastapi-filebased-routing/releases/tag/v0.2.0
61
+ [0.1.0]: https://github.com/irudi/fastapi-filebased-routing/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 irudi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,359 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-filebased-routing
3
+ Version: 1.0.0
4
+ Summary: Next.js-style file-based routing for FastAPI
5
+ Project-URL: Homepage, https://github.com/irudi/fastapi-filebased-routing
6
+ Project-URL: Documentation, https://github.com/irudi/fastapi-filebased-routing#readme
7
+ Project-URL: Repository, https://github.com/irudi/fastapi-filebased-routing
8
+ Project-URL: Bug Tracker, https://github.com/irudi/fastapi-filebased-routing/issues
9
+ Author: irudi
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: fastapi,file-based,filesystem,nextjs,routing
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.13
23
+ Requires-Dist: fastapi>=0.115.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: httpx>=0.27; extra == 'dev'
26
+ Requires-Dist: mypy>=1.10; extra == 'dev'
27
+ Requires-Dist: pre-commit>=4.0; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
29
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.8; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # fastapi-filebased-routing
35
+
36
+ Next.js-style file-based routing for FastAPI
37
+
38
+ [![PyPI version](https://img.shields.io/pypi/v/fastapi-filebased-routing.svg)](https://pypi.org/project/fastapi-filebased-routing/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/fastapi-filebased-routing.svg)](https://pypi.org/project/fastapi-filebased-routing/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
41
+
42
+ 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.
43
+
44
+ ## Table of Contents
45
+
46
+ - [🚀 Installation & Quickstart](#-installation--quickstart)
47
+ - [📁 File-Based Routing Explained](#-file-based-routing-explained)
48
+ - [📍 Route Handlers](#-route-handlers)
49
+ - [🔗 Middleware](#-middleware)
50
+ - [📦 Examples](#-examples)
51
+ - [📖 API Reference](#-api-reference)
52
+ - [💡 Why This Plugin?](#-why-this-plugin)
53
+ - [🤝 Contributing](#-contributing)
54
+
55
+ ## 🚀 Installation & Quickstart
56
+
57
+ Requires **Python 3.13+** and **FastAPI 0.115.0+**.
58
+
59
+ ```bash
60
+ pip install fastapi-filebased-routing
61
+ ```
62
+
63
+ ```python
64
+ from fastapi import FastAPI
65
+ from fastapi_filebased_routing import create_router_from_path
66
+
67
+ app = FastAPI()
68
+ app.include_router(create_router_from_path("app"))
69
+ ```
70
+
71
+ That's it. Every `route.py` file in your `app/` directory is automatically discovered and registered.
72
+
73
+ ## 📁 File-Based Routing Explained
74
+
75
+ Your directory structure defines your URL routes. Given `create_router_from_path("app")`:
76
+
77
+ ```
78
+ app/
79
+ ├── _middleware.py # directory middleware → ALL routes
80
+ ├── health/
81
+ │ └── route.py # get → /health
82
+ ├── api/
83
+ │ ├── _middleware.py # directory middleware → /api/**
84
+ │ ├── [[version]]/
85
+ │ │ └── route.py # → /api and /api/{version}
86
+ │ └── users/
87
+ │ ├── route.py # file-level middleware + handlers
88
+ │ └── [user_id]/
89
+ │ └── route.py # handler-level middleware via class
90
+ ├── files/
91
+ │ └── [...path]/
92
+ │ └── route.py # catch-all route
93
+ ├── ws/
94
+ │ └── chat/
95
+ │ └── route.py # websocket handler
96
+ └── (admin)/ # group: excluded from URL
97
+ ├── _middleware.py # directory middleware → /settings/**
98
+ └── settings/
99
+ └── route.py # → /settings
100
+ ```
101
+
102
+ Each `route.py` exports [route handlers](#-route-handlers). Each `_middleware.py` defines [directory middleware](#directory-level-middleware).
103
+
104
+ ### Route Conventions
105
+
106
+ | Convention | Route Example | URL | Handler Parameter |
107
+ |------------|---------------|-----|-------------------|
108
+ | `users/` | `app/users/route.py` | `/users` | — |
109
+ | `[id]/` | `app/users/[id]/route.py` | `/users/123` | `id: str` |
110
+ | `[[version]]/` | `app/api/[[version]]/route.py` | `/api` and `/api/v2` | `version: str \| None` |
111
+ | `[...path]/` | `app/files/[...path]/route.py` | `/files/a/b/c` | `path: str` |
112
+ | `(group)/` | `app/(admin)/settings/route.py` | `/settings` | — |
113
+
114
+ **Files:** `route.py` contains [handlers](#-route-handlers). `_middleware.py` contains [directory middleware](#directory-level-middleware) that cascades to all subdirectories.
115
+
116
+ ## 📍 Route Handlers
117
+
118
+ 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.
119
+
120
+ ```python
121
+ # app/api/users/route.py
122
+ from fastapi_filebased_routing import route
123
+
124
+ # Module-level metadata (applies to all handlers in this file)
125
+ TAGS = ["users"] # auto-derived from first path segment if omitted
126
+ SUMMARY = "User management endpoints" # OpenAPI summary
127
+ DEPRECATED = True # mark all handlers as deprecated
128
+
129
+ # File-level middleware (applies to all handlers in this file, does NOT cascade to subdirectories)
130
+ middleware = [rate_limit(100)]
131
+
132
+ # Simple handler — just a function
133
+ async def get():
134
+ """List users."""
135
+ return {"users": []}
136
+
137
+ # Configured handler — per-handler control over metadata and middleware
138
+ class post(route):
139
+ status_code = 200 # override convention-based 201
140
+ tags = ["admin"] # override module-level TAGS
141
+ summary = "Create a user" # override module-level SUMMARY
142
+ deprecated = True # override module-level DEPRECATED
143
+ middleware = [require_role("admin")] # or use inline: async def middleware(request, call_next): ...
144
+
145
+ async def handler(name: str):
146
+ return {"name": name}
147
+ ```
148
+
149
+ Both styles coexist freely. Directory bracket names (e.g., `[user_id]`) become path parameters automatically injected into handler signatures. See [`examples/`](examples/) for complete working projects.
150
+
151
+ ## 🔗 Middleware
152
+
153
+ 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.
154
+
155
+ All middleware functions use the same signature:
156
+
157
+ ```python
158
+ async def my_middleware(request, call_next):
159
+ # Before handler
160
+ response = await call_next(request)
161
+ # After handler
162
+ return response
163
+ ```
164
+
165
+ Middleware must be `async`. Sync middleware raises a validation error at startup.
166
+
167
+ ### Directory-Level Middleware
168
+
169
+ 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:
170
+
171
+ **List form** — multiple middleware functions:
172
+
173
+ ```python
174
+ # app/api/_middleware.py — applies to all routes under /api/**
175
+ from app.auth import auth_required
176
+ from app.logging import request_logger
177
+
178
+ middleware = [auth_required, request_logger]
179
+ ```
180
+
181
+ **Single function form** — one inline middleware:
182
+
183
+ ```python
184
+ # app/_middleware.py — root-level timing middleware
185
+ import time
186
+
187
+ async def middleware(request, call_next):
188
+ start = time.monotonic()
189
+ response = await call_next(request)
190
+ response.headers["X-Response-Time"] = f"{time.monotonic() - start:.4f}"
191
+ return response
192
+ ```
193
+
194
+ Pick one form per file. If both are defined, standard Python name resolution applies — the last assignment to `middleware` wins.
195
+
196
+ 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.
197
+
198
+ ### File-Level Middleware
199
+
200
+ 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.
201
+
202
+ ```python
203
+ # app/api/users/route.py
204
+ middleware = [rate_limit(100)] # applies to get and post below, not to /api/users/[user_id]
205
+
206
+ async def get():
207
+ """List users. Rate limited."""
208
+ return {"users": []}
209
+
210
+ async def post(name: str):
211
+ """Create user. Rate limited."""
212
+ return {"name": name}
213
+ ```
214
+
215
+ ### Handler-Level Middleware
216
+
217
+ `class handler(route):` blocks support a `middleware` attribute — as a list or a single inline function:
218
+
219
+ ```python
220
+ # app/api/orders/route.py
221
+ from fastapi_filebased_routing import route
222
+
223
+ class post(route):
224
+ async def middleware(request, call_next):
225
+ if not request.headers.get("X-Idempotency-Key"):
226
+ from fastapi.responses import JSONResponse
227
+ return JSONResponse(
228
+ {"error": "X-Idempotency-Key header required"},
229
+ status_code=400,
230
+ )
231
+ return await call_next(request)
232
+
233
+ async def handler(order: dict):
234
+ """Create order. Requires idempotency key."""
235
+ return {"order_id": "abc-123", **order}
236
+ ```
237
+
238
+ ### Execution Order
239
+
240
+ When a request hits a route with middleware at multiple levels, they execute in this order:
241
+
242
+ ```
243
+ Directory middleware (root → leaf)
244
+ → File-level middleware
245
+ → Handler-level middleware
246
+ → Handler function
247
+ ← Handler-level middleware
248
+ ← File-level middleware
249
+ ← Directory middleware
250
+ ```
251
+
252
+ 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`:
253
+
254
+ ```python
255
+ async def auth_guard(request, call_next):
256
+ if not request.headers.get("Authorization"):
257
+ return JSONResponse({"error": "unauthorized"}, status_code=401)
258
+ return await call_next(request)
259
+ ```
260
+
261
+ ## 📦 Examples
262
+
263
+ See the [`examples/`](examples/) directory for runnable projects:
264
+
265
+ - **[`basic/`](examples/basic/)** — Routing fundamentals: static, dynamic, CRUD
266
+ - **[`middleware/`](examples/middleware/)** — All three middleware layers in action
267
+ - **[`advanced/`](examples/advanced/)** — Optional params, catch-all, route groups, WebSockets
268
+
269
+ ## 📖 API Reference
270
+
271
+ ### `create_router_from_path`
272
+
273
+ ```python
274
+ def create_router_from_path(
275
+ base_path: str | Path,
276
+ *,
277
+ prefix: str = "",
278
+ ) -> APIRouter
279
+ ```
280
+
281
+ Create a FastAPI APIRouter from a directory of `route.py` files.
282
+
283
+ **Parameters:**
284
+ - `base_path` (str | Path): Root directory containing route.py files
285
+ - `prefix` (str, optional): URL prefix for all discovered routes (default: "")
286
+
287
+ **Returns:**
288
+ - `APIRouter`: A FastAPI router with all discovered routes registered
289
+
290
+ **Raises:**
291
+ - `RouteDiscoveryError`: If base_path doesn't exist or isn't a directory
292
+ - `RouteValidationError`: If a route file has invalid exports or parameters
293
+ - `DuplicateRouteError`: If two route files resolve to the same path+method
294
+ - `PathParseError`: If a directory name has invalid syntax
295
+ - `MiddlewareValidationError`: If a `_middleware.py` file fails to import or contains invalid middleware
296
+
297
+ **Example:**
298
+
299
+ ```python
300
+ from fastapi import FastAPI
301
+ from fastapi_filebased_routing import create_router_from_path
302
+
303
+ app = FastAPI()
304
+
305
+ # Basic usage
306
+ app.include_router(create_router_from_path("app"))
307
+
308
+ # With prefix
309
+ app.include_router(create_router_from_path("app", prefix="/api/v1"))
310
+
311
+ # Multiple routers
312
+ app.include_router(create_router_from_path("app/public"))
313
+ app.include_router(create_router_from_path("app/admin", prefix="/admin"))
314
+ ```
315
+
316
+ ### `route`
317
+
318
+ Base class for handler-level middleware and metadata configuration. Uses a metaclass that returns a `RouteConfig` instead of a class.
319
+
320
+ ```python
321
+ from fastapi_filebased_routing import route
322
+
323
+ class get(route):
324
+ middleware = [auth_required]
325
+ tags = ["users"]
326
+ summary = "Get user details"
327
+
328
+ async def handler(user_id: str):
329
+ return {"user_id": user_id}
330
+
331
+ # `get` is now a RouteConfig, not a class
332
+ # `get(user_id="123")` calls the handler directly
333
+ ```
334
+
335
+ ## 💡 Why This Plugin?
336
+
337
+ FastAPI developers building medium-to-large APIs face these problems:
338
+
339
+ 1. **Manual route registration is tedious.** Every endpoint requires updating a centralized router file.
340
+ 2. **Route discoverability degrades.** Finding the handler for `/api/v1/users/{id}` requires searching across files.
341
+ 3. **Middleware wiring is repetitive.** Applying auth to 20 admin endpoints means 20 copies of `Depends(require_admin)`.
342
+ 4. **Full-stack developers experience friction.** Next.js has file-based routing, FastAPI requires manual wiring.
343
+
344
+ This plugin solves all four with:
345
+
346
+ - Zero-configuration route discovery from directory structure
347
+ - Three-layer middleware system (directory, file, handler) with cascading inheritance
348
+ - Next.js feature parity: dynamic params, optional params, catch-all, route groups
349
+ - Convention over configuration for status codes, tags, and metadata
350
+ - Battle-tested security (path traversal protection, symlink validation)
351
+ - WebSocket support, sync and async handlers
352
+ - Hot reload compatible (`uvicorn --reload` works out of the box)
353
+ - Full mypy strict mode support, coexists with manual FastAPI routing
354
+
355
+ ## 🤝 Contributing
356
+
357
+ This plugin is extracted from a production codebase and is actively maintained. Issues, feature requests, and pull requests are welcome.
358
+
359
+ GitHub: https://github.com/rsmdt/fastapi-filebased-routing.py