docs+feat: FastAPI parity — add_api_route, on_event, mount, app.state, OpenAPI metadata + full docs rewrite
swadhinbiswas committed 3 months ago
d60346a
1 parent 7329e8f
commit d60346a
6 files changed
+514 -163
Showing 6 changed files+514-163
crates/justapi-py/python/justapi/app.pydocs_site/src/assets/architectureoverview.pngdocs_site/src/content/docs/api-reference/apirouter.mddocs_site/src/content/docs/api-reference/justapiapp.mddocs_site/src/content/docs/api-reference/routing.mddocs_site/src/content/docs/tutorials/routing-subrouters.md
▼crates/justapi-py/python/justapi/app.py
+116-3
| @@ -233,6 +233,11 @@ def adaptive_batch(max_size: int = 32, window_ms: int = 10): | ||
233 | 233 | return func |
234 | 234 | return wrapper |
235 | 235 | |
236 | +class _AppState: | |
237 | + def __setattr__(self, name, value): | |
238 | + self.__dict__[name] = value | |
239 | + | |
240 | + | |
236 | 241 | class JustAPIApp: |
237 | 242 | def __init__( |
238 | 243 | self, |
| @@ -241,6 +246,14 @@ class JustAPIApp: | ||
241 | 246 | description: str = None, |
242 | 247 | openapi_tags: list = None, |
243 | 248 | dependencies: typing.List[Depends] = None, |
249 | + servers: list = None, | |
250 | + contact: dict = None, | |
251 | + license_info: dict = None, | |
252 | + terms_of_service: str = None, | |
253 | + openapi_url: str = "/openapi.json", | |
254 | + docs_url: str = "/docs", | |
255 | + redoc_url: str = "/redoc", | |
256 | + scalar_url: str = "/scalar", | |
244 | 257 | ): |
245 | 258 | self._app = _JustAPIApp() |
246 | 259 | self.title = title |
| @@ -248,10 +261,21 @@ class JustAPIApp: | ||
248 | 261 | self.description = description |
249 | 262 | self.openapi_tags = openapi_tags or [] |
250 | 263 | self.dependencies = dependencies or [] |
264 | + self.servers = servers | |
265 | + self.contact = contact | |
266 | + self.license_info = license_info | |
267 | + self.terms_of_service = terms_of_service | |
268 | + self.openapi_url = openapi_url | |
269 | + self.docs_url = docs_url | |
270 | + self.redoc_url = redoc_url | |
271 | + self.scalar_url = scalar_url | |
251 | 272 | self.exception_handlers = {} |
252 | 273 | self.middlewares = [] |
253 | 274 | self._named_routes = {} |
254 | 275 | self.routes = [] |
276 | + self._startup_handlers = [] | |
277 | + self._shutdown_handlers = [] | |
278 | + self.state = _AppState() | |
255 | 279 | |
256 | 280 | |
257 | 281 | # Built-in probe/metrics endpoints for the Python app. |
| @@ -1203,6 +1227,79 @@ class JustAPIApp: | ||
1203 | 1227 | return func |
1204 | 1228 | return decorator |
1205 | 1229 | |
1230 | + def add_api_route(self, path: str, endpoint: typing.Callable, methods: typing.List[str] = None, **kwargs): | |
1231 | + """Register a route non-decoratively (FastAPI/Starlette parity). | |
1232 | + | |
1233 | + Usage:: | |
1234 | + | |
1235 | + def my_handler(request): | |
1236 | + return {"ok": True} | |
1237 | + | |
1238 | + app.add_api_route("/path", my_handler, methods=["GET", "POST"]) | |
1239 | + """ | |
1240 | + for method in (methods or ["GET"]): | |
1241 | + m = method.upper() | |
1242 | + getattr(self, m.lower())(path, endpoint, **kwargs) | |
1243 | + | |
1244 | + def add_api_websocket_route(self, path: str, endpoint: typing.Callable): | |
1245 | + """Register a WebSocket route non-decoratively (FastAPI/Starlette parity). | |
1246 | + | |
1247 | + Usage:: | |
1248 | + | |
1249 | + async def my_ws(ws): | |
1250 | + await ws.accept() | |
1251 | + await ws.send_text("connected") | |
1252 | + | |
1253 | + app.add_api_websocket_route("/ws", my_ws) | |
1254 | + """ | |
1255 | + self.websocket(path, endpoint) | |
1256 | + | |
1257 | + def mount(self, path: str, app_or_directory, name: str = None): | |
1258 | + """Mount a sub-application or static directory at a path. | |
1259 | + | |
1260 | + Mirrors FastAPI/Starlette ``app.mount()``. | |
1261 | + | |
1262 | + Usage:: | |
1263 | + | |
1264 | + # Mount a static directory | |
1265 | + app.mount("/static", "static", name="static") | |
1266 | + | |
1267 | + # Mount an APIRouter as a sub-app | |
1268 | + app.mount("/api/v1", users_router) | |
1269 | + """ | |
1270 | + if isinstance(app_or_directory, str): | |
1271 | + return self.frontend(path, app_or_directory) | |
1272 | + if hasattr(app_or_directory, "routes"): | |
1273 | + return self.include_router(app_or_directory, prefix=path) | |
1274 | + raise ValueError( | |
1275 | + "mount() expects a directory path (str) or an APIRouter " | |
1276 | + f"instance, got {type(app_or_directory).__name__}" | |
1277 | + ) | |
1278 | + | |
1279 | + def on_event(self, event_type: str): | |
1280 | + """Register a startup/shutdown event handler (FastAPI parity). | |
1281 | + | |
1282 | + Usage:: | |
1283 | + | |
1284 | + @app.on_event("startup") | |
1285 | + def init_db(): | |
1286 | + ... | |
1287 | + | |
1288 | + @app.on_event("shutdown") | |
1289 | + def close_pool(): | |
1290 | + ... | |
1291 | + """ | |
1292 | + if event_type not in ("startup", "shutdown"): | |
1293 | + raise ValueError(f"event_type must be 'startup' or 'shutdown', got {event_type!r}") | |
1294 | + | |
1295 | + def decorator(func): | |
1296 | + if event_type == "startup": | |
1297 | + self._startup_handlers.append(func) | |
1298 | + else: | |
1299 | + self._shutdown_handlers.append(func) | |
1300 | + return func | |
1301 | + return decorator | |
1302 | + | |
1206 | 1303 | def include_router(self, router, prefix: str = "", tags: typing.List[str] = None): |
1207 | 1304 | for route in router.routes: |
1208 | 1305 | path = prefix + route["path"] |
| @@ -1383,6 +1480,14 @@ class JustAPIApp: | ||
1383 | 1480 | return self |
1384 | 1481 | |
1385 | 1482 | def run(self, addr: str, max_body_size: int = 50 * 1024 * 1024): |
1483 | + # Execute startup handlers (FastAPI on_event parity). | |
1484 | + for handler in self._startup_handlers: | |
1485 | + if inspect.iscoroutinefunction(handler): | |
1486 | + import asyncio | |
1487 | + asyncio.run(handler()) | |
1488 | + else: | |
1489 | + handler() | |
1490 | + | |
1386 | 1491 | # Start the native Rust scheduler (if any jobs were registered) so cron |
1387 | 1492 | # and interval jobs run for the lifetime of the server. |
1388 | 1493 | from ._justapi import Scheduler |
| @@ -1391,10 +1496,18 @@ class JustAPIApp: | ||
1391 | 1496 | try: |
1392 | 1497 | self._app.run(addr, max_body_size) |
1393 | 1498 | except KeyboardInterrupt: |
1394 | - # Ctrl-C: the Rust runtime already performs a graceful drain/shutdown | |
1395 | - # on the signal; swallow the Python traceback so the user just sees | |
1396 | - # the clean "shutting down" log lines instead of a stack trace. | |
1397 | 1499 | pass |
1500 | + finally: | |
1501 | + # Execute shutdown handlers (FastAPI on_event parity). | |
1502 | + for handler in self._shutdown_handlers: | |
1503 | + try: | |
1504 | + if inspect.iscoroutinefunction(handler): | |
1505 | + import asyncio | |
1506 | + asyncio.run(handler()) | |
1507 | + else: | |
1508 | + handler() | |
1509 | + except Exception: | |
1510 | + pass | |
1398 | 1511 | |
1399 | 1512 | def schedule(self, cron_expr, func, *args, **kwargs): |
1400 | 1513 | """Register a cron job (native Rust scheduler). |
1401 | 1514 | |
▼docs_site/src/assets/architectureoverview.png
+0-0
Binary file not shown.
▼docs_site/src/content/docs/api-reference/apirouter.md
+64-21
| @@ -1,7 +1,7 @@ | ||
1 | 1 | --- |
2 | 2 | title: APIRouter |
3 | -description: "API reference for APIRouter in JustAPI, the FastAPI alternative — modular router for grouping related routes in separate files." | |
4 | -keywords: [apirouter, fastapi alternative, justapi, modular router, route groups, sub-routers] | |
3 | +description: API reference for APIRouter in JustAPI, the FastAPI alternative — modular router for grouping related routes in separate files, with sub-router support and include_router. | |
4 | +keywords: [apirouter, fastapi alternative, justapi, modular router, route groups, sub-routers, include_router, url_for] | |
5 | 5 | --- |
6 | 6 | |
7 | 7 | `APIRouter` lets you organize routes into modular groups, then include them in the main application. |
| @@ -14,6 +14,12 @@ from justapi import APIRouter | ||
14 | 14 | router = APIRouter( |
15 | 15 | prefix="/api/v1", |
16 | 16 | tags=["API v1"], |
17 | + dependencies=None, | |
18 | + middlewares=None, | |
19 | + responses=None, | |
20 | + deprecated=False, | |
21 | + name=None, | |
22 | + include_in_schema=True, | |
17 | 23 | ) |
18 | 24 | ``` |
19 | 25 | |
| @@ -23,29 +29,46 @@ router = APIRouter( | ||
23 | 29 | |---|---|---|---| |
24 | 30 | | `prefix` | `str` | `""` | URL prefix applied to all routes | |
25 | 31 | | `tags` | `list[str]` | `None` | OpenAPI tags for all routes | |
32 | +| `dependencies` | `list[Depends]` | `None` | Router-level dependencies (applied to all routes) | | |
33 | +| `middlewares` | `list[Callable]` | `None` | Router-level middleware functions | | |
34 | +| `responses` | `dict` | `None` | Default OpenAPI responses | | |
35 | +| `deprecated` | `bool` | `False` | Mark all routes as deprecated | | |
36 | +| `name` | `str` | `None` | Router name | | |
37 | +| `include_in_schema` | `bool` | `True` | Include routes in OpenAPI | | |
26 | 38 | |
27 | 39 | ## Methods |
28 | 40 | |
29 | 41 | `APIRouter` supports the same route decorators as `JustAPIApp`: |
30 | 42 | |
31 | -| Method | Signature | | |
43 | +| Method | Description | | |
32 | 44 | |---|---| |
33 | -| `get(path, dependencies, tags)` | Same as `JustAPIApp.get()` | | |
34 | -| `post(path, body_schema, schema, dependencies, tags, native)` | Same as `JustAPIApp.post()` | | |
35 | -| `put(path, body_schema, schema, dependencies, tags, native)` | Same as `JustAPIApp.put()` | | |
36 | -| `patch(path, body_schema, schema, dependencies, tags, native)` | Same as `JustAPIApp.patch()` | | |
37 | -| `delete(path, dependencies, tags)` | Same as `JustAPIApp.delete()` | | |
38 | -| `websocket(path)` | Same as `JustAPIApp.websocket()` | | |
45 | +| `get(path, ...)` | Register GET route | | |
46 | +| `post(path, body_schema, ...)` | Register POST route | | |
47 | +| `put(path, body_schema, ...)` | Register PUT route | | |
48 | +| `patch(path, body_schema, ...)` | Register PATCH route | | |
49 | +| `delete(path, ...)` | Register DELETE route | | |
50 | +| `head(path, ...)` | Register HEAD route | | |
51 | +| `options(path, ...)` | Register OPTIONS route | | |
52 | +| `trace(path, ...)` | Register TRACE route | | |
53 | +| `query(path, body_schema, ...)` | Register QUERY method (RFC 10008) | | |
54 | +| `websocket(path)` | Register WebSocket handler | | |
55 | +| `sse(path)` | Register Server-Sent Events | | |
56 | +| `route(path, methods)` | Register multi-method route | | |
57 | +| `frontend(path, directory)` | Static frontend mount | | |
58 | +| `url_for(name, **path_params)` | Build URL from named route | | |
59 | + | |
60 | +Each method accepts the same parameters as the corresponding `JustAPIApp` method (dependencies, tags, summary, description, deprecated, status_code, responses, operation_id, openapi_extra, include_in_schema, name, etc.). | |
39 | 61 | |
40 | 62 | ## Including Routers |
41 | 63 | |
64 | +### In the Main App | |
65 | + | |
42 | 66 | ```python |
43 | 67 | from justapi import JustAPIApp |
44 | 68 | from app.routers.users import router as users_router |
45 | -from app.routers.products import router as products_router | |
46 | 69 | |
47 | 70 | app = JustAPIApp() |
48 | -app.include_router(users_router, prefix="/api/v1") | |
71 | +app.include_router(users_router, prefix="/api/v1", tags=["Users"]) | |
49 | 72 | ``` |
50 | 73 | |
51 | 74 | ### `include_router()` Parameters |
| @@ -53,24 +76,40 @@ app.include_router(users_router, prefix="/api/v1") | ||
53 | 76 | | Parameter | Type | Default | Description | |
54 | 77 | |---|---|---|---| |
55 | 78 | | `router` | `APIRouter` | required | The router to include | |
56 | -| `prefix` | `str` | `""` | Additional prefix applied on top of the router's own prefix | | |
79 | +| `prefix` | `str` | `""` | Additional prefix on top of the router's own prefix | | |
80 | +| `tags` | `list[str]` | `None` | Tags prepended to each route's existing tags | | |
57 | 81 | |
58 | -## URL Construction | |
82 | +### Sub-Routers (Nested) | |
59 | 83 | |
60 | -The final URL for each route is: `include_router_prefix` + `router.prefix` + `route_path` | |
84 | +APIRouter instances can include other APIRouter instances, enabling nested route hierarchies: | |
61 | 85 | |
62 | 86 | ```python |
63 | -router = APIRouter(prefix="/users") | |
87 | +from justapi import APIRouter | |
88 | + | |
89 | +users_router = APIRouter(prefix="/users") | |
64 | 90 | |
65 | -@router.get("/{user_id}") | |
91 | +@users_router.get("/{user_id}") | |
66 | 92 | def get_user(request, user_id: int): ... |
67 | 93 | |
68 | -app.include_router(router, prefix="/api/v1") | |
94 | +admin_router = APIRouter(prefix="/admin") | |
95 | +admin_router.include_router(users_router) | |
69 | 96 | |
70 | -# Final URL: /api/v1/users/{user_id} | |
97 | +# Final URL: /admin/users/{user_id} | |
71 | 98 | ``` |
72 | 99 | |
73 | -## Router-Level Dependencies | |
100 | +## URL Construction | |
101 | + | |
102 | +The final URL for each route follows FastAPI merge rules: | |
103 | + | |
104 | +``` | |
105 | +include_router_prefix + router.prefix + route_path | |
106 | +``` | |
107 | + | |
108 | +Router-level tags and dependencies are merged with route-level ones. | |
109 | + | |
110 | +## Router-Level Dependencies and Middleware | |
111 | + | |
112 | +Dependencies and middleware declared on the router apply to all routes within it: | |
74 | 113 | |
75 | 114 | ```python |
76 | 115 | from justapi import APIRouter, Depends, Header, HTTPException |
| @@ -79,9 +118,13 @@ def require_token(authorization: str = Header(...)): | ||
79 | 118 | if not authorization.startswith("Bearer "): |
80 | 119 | raise HTTPException(401) |
81 | 120 | |
82 | -admin_router = APIRouter(prefix="/admin", tags=["Admin"]) | |
121 | +admin_router = APIRouter( | |
122 | + prefix="/admin", | |
123 | + tags=["Admin"], | |
124 | + dependencies=[Depends(require_token)], | |
125 | +) | |
83 | 126 | |
84 | -@admin_router.get("/users", dependencies=[Depends(require_token)]) | |
127 | +@admin_router.get("/users") | |
85 | 128 | def list_users(request): |
86 | 129 | return [{"id": 1}] |
87 | 130 | ``` |
88 | 131 | |
▼docs_site/src/content/docs/api-reference/justapiapp.md
+173-57
| @@ -1,7 +1,7 @@ | ||
1 | 1 | --- |
2 | 2 | title: JustAPIApp |
3 | -description: "API reference for JustAPIApp — the core application class of the FastAPI alternative JustAPI. Register routes, middleware, plugins, and run the server." | |
4 | -keywords: [justapiapp, fastapi alternative, justapi, application class, route registration, middleware] | |
3 | +description: API reference for JustAPIApp — the core application class of the FastAPI alternative JustAPI. Register routes, middleware, plugins, and run the server. | |
4 | +keywords: [justapiapp, fastapi alternative, justapi, application class, route registration, middleware, add_api_route, on_event, mount, app.state] | |
5 | 5 | --- |
6 | 6 | |
7 | 7 | `JustAPIApp` is the central class of any JustAPI application. It manages route registration, middleware chains, plugin lifecycle, database connections, and server startup. |
| @@ -15,6 +15,14 @@ app = JustAPIApp( | ||
15 | 15 | title="My API", |
16 | 16 | version="1.0.0", |
17 | 17 | description="A high-performance API powered by JustAPI", |
18 | + servers=None, | |
19 | + contact=None, | |
20 | + license_info=None, | |
21 | + terms_of_service=None, | |
22 | + openapi_url="/openapi.json", | |
23 | + docs_url="/docs", | |
24 | + redoc_url="/redoc", | |
25 | + scalar_url="/scalar", | |
18 | 26 | ) |
19 | 27 | ``` |
20 | 28 | |
| @@ -24,81 +32,197 @@ app = JustAPIApp( | ||
24 | 32 | |---|---|---|---| |
25 | 33 | | `title` | `str` | `"JustAPI"` | API title for OpenAPI docs | |
26 | 34 | | `version` | `str` | `"1.0.0"` | API version for OpenAPI docs | |
27 | -| `description` | `str` | `""` | API description for OpenAPI docs | | |
35 | +| `description` | `str` | `""` | API description | | |
36 | +| `servers` | `list[dict]` | `None` | OpenAPI servers array | | |
37 | +| `contact` | `dict` | `None` | OpenAPI contact info `{"name": ..., "url": ..., "email": ...}` | | |
38 | +| `license_info` | `dict` | `None` | OpenAPI license info `{"name": ..., "url": ...}` | | |
39 | +| `terms_of_service` | `str` | `None` | OpenAPI terms of service URL | | |
40 | +| `openapi_url` | `str` | `"/openapi.json"` | OpenAPI schema URL | | |
41 | +| `docs_url` | `str` | `"/docs"` | Swagger UI URL | | |
42 | +| `redoc_url` | `str` | `"/redoc"` | ReDoc URL | | |
43 | +| `scalar_url` | `str` | `"/scalar"` | Scalar UI URL | | |
44 | + | |
45 | +## Application State | |
46 | + | |
47 | +Use `app.state` to store application-level data (FastAPI parity): | |
48 | + | |
49 | +```python | |
50 | +app.state.db_pool = create_pool() | |
51 | +app.state.cache = RedisCache() | |
52 | +``` | |
28 | 53 | |
29 | 54 | ## Route Registration |
30 | 55 | |
31 | -| Method | Returns | Description | | |
32 | -|---|---|---| | |
33 | -| `get(path, dependencies, tags)` | decorator | Register GET route | | |
34 | -| `post(path, body_schema, schema, dependencies, tags, native)` | decorator | Register POST route | | |
35 | -| `put(path, body_schema, schema, dependencies, tags, native)` | decorator | Register PUT route | | |
36 | -| `patch(path, body_schema, schema, dependencies, tags, native)` | decorator | Register PATCH route | | |
37 | -| `delete(path, dependencies, tags)` | decorator | Register DELETE route | | |
38 | -| `websocket(path)` | decorator | Register WebSocket handler | | |
39 | -| `include_router(router, prefix)` | `None` | Include routes from APIRouter | | |
56 | +### Decorator Style | |
57 | + | |
58 | +| Method | Description | | |
59 | +|---|---| | |
60 | +| `get(path, ...)` | Register GET route | | |
61 | +| `post(path, body_schema, ...)` | Register POST route | | |
62 | +| `put(path, body_schema, ...)` | Register PUT route | | |
63 | +| `patch(path, body_schema, ...)` | Register PATCH route | | |
64 | +| `delete(path, ...)` | Register DELETE route | | |
65 | +| `head(path, ...)` | Register HEAD route | | |
66 | +| `options(path, ...)` | Register OPTIONS route | | |
67 | +| `trace(path, ...)` | Register TRACE route | | |
68 | +| `query(path, body_schema, ...)` | Register QUERY method (RFC 10008) | | |
69 | +| `websocket(path)` | Register WebSocket handler | | |
70 | +| `sse(path)` | Register Server-Sent Events route | | |
71 | +| `route(path, methods)` | Register multi-method route | | |
72 | + | |
73 | +### Non-Decorator Style (FastAPI Parity) | |
74 | + | |
75 | +Register routes programmatically without decorators: | |
76 | + | |
77 | +```python | |
78 | +def my_handler(request): | |
79 | + return {"ok": True} | |
80 | + | |
81 | +app.add_api_route("/items", my_handler, methods=["GET", "POST"]) | |
82 | +app.add_api_websocket_route("/ws", my_ws_handler) | |
83 | +``` | |
40 | 84 | |
41 | -### Route Parameter Details | |
85 | +### Route Parameters | |
42 | 86 | |
43 | 87 | | Parameter | Type | Default | Description | |
44 | 88 | |---|---|---|---| |
45 | 89 | | `path` | `str` | required | URL path template (e.g., `/items/{id}`) | |
46 | -| `body_schema` | `Schema` subclass | `None` | Schema for request body validation in Rust | | |
47 | -| `schema` | `Schema` subclass | `None` | Alias for `body_schema` | | |
90 | +| `body_schema` / `schema` | `Schema` subclass | `None` | Schema for request body validation in Rust | | |
48 | 91 | | `dependencies` | `list[Depends]` | `None` | Route-level dependencies | |
92 | +| `middlewares` | `list[Callable]` | `None` | Route-level middlewares | | |
49 | 93 | | `tags` | `list[str]` | `None` | OpenAPI tags | |
94 | +| `summary` | `str` | `None` | OpenAPI operation summary | | |
95 | +| `description` | `str` | `None` | OpenAPI operation description | | |
96 | +| `deprecated` | `bool` | `False` | Mark as deprecated in OpenAPI | | |
97 | +| `status_code` | `int` | `None` | Default response status code | | |
98 | +| `responses` | `dict` | `None` | Additional OpenAPI responses | | |
99 | +| `operation_id` | `str` | `None` | OpenAPI operation ID | | |
100 | +| `openapi_extra` | `dict` | `None` | Extra OpenAPI metadata | | |
101 | +| `include_in_schema` | `bool` | `True` | Exclude from OpenAPI | | |
102 | +| `name` | `str` | `None` | Named route (for `url_for()`) | | |
50 | 103 | | `native` | `bool` | `False` | Execute entirely in Rust (724k+ RPS) | |
51 | 104 | |
105 | +## Sub-Applications | |
106 | + | |
107 | +Mount APIRouter instances or static directories: | |
108 | + | |
109 | +```python | |
110 | +# Mount an APIRouter | |
111 | +app.mount("/api/v1", users_router) | |
112 | + | |
113 | +# Mount a static directory | |
114 | +app.mount("/static", "static", name="static") | |
115 | +``` | |
116 | + | |
117 | +## Lifecycle Events | |
118 | + | |
119 | +Register startup and shutdown handlers (FastAPI parity): | |
120 | + | |
121 | +```python | |
122 | +@app.on_event("startup") | |
123 | +def init_db(): | |
124 | + app.state.db = create_connection() | |
125 | + | |
126 | +@app.on_event("shutdown") | |
127 | +def close_db(): | |
128 | + app.state.db.close() | |
129 | +``` | |
130 | + | |
131 | +## Including Routers | |
132 | + | |
133 | +```python | |
134 | +app.include_router(router, prefix="/api/v1", tags=["API"]) | |
135 | +``` | |
136 | + | |
137 | +| Parameter | Type | Default | Description | | |
138 | +|---|---|---|---| | |
139 | +| `router` | `APIRouter` | required | The router to include | | |
140 | +| `prefix` | `str` | `""` | Additional URL prefix | | |
141 | +| `tags` | `list[str]` | `None` | Additional OpenAPI tags | | |
142 | + | |
143 | +## Controller Classes | |
144 | + | |
145 | +```python | |
146 | +from justapi import Controller, controller, route_get | |
147 | + | |
148 | +@controller("/users") | |
149 | +class UserController(Controller): | |
150 | + @route_get("/{user_id}") | |
151 | + def get_user(self, request, user_id: int): | |
152 | + return {"user_id": user_id} | |
153 | + | |
154 | +app.include_controller(UserController) | |
155 | +``` | |
156 | + | |
52 | 157 | ## Middleware & Security |
53 | 158 | |
54 | 159 | | Method | Description | |
55 | 160 | |---|---| |
56 | 161 | | `middleware(type)` | Decorator to register HTTP middleware (`"http"`) | |
57 | -| `add_cors(allow_origins, allow_methods, allow_headers, allow_credentials)` | Configure CORS | | |
162 | +| `add_cors(allow_origins, ...)` | Configure CORS | | |
58 | 163 | | `enable_secure_headers(with_hsts)` | Add security headers | |
59 | 164 | | `add_middleware(middleware_cls, **kwargs)` | Add middleware class (Starlette parity) | |
165 | +| `add_exception_handler(exc_class, handler)` | Register custom exception handler | | |
60 | 166 | |
61 | 167 | ## Resilience |
62 | 168 | |
63 | 169 | | Method | Description | |
64 | 170 | |---|---| |
65 | -| `enable_circuit_breaker(failure_threshold, reset_timeout_ms)` | Enable circuit breaker pattern | | |
171 | +| `enable_circuit_breaker(failure_threshold, reset_timeout_ms)` | Enable circuit breaker | | |
66 | 172 | | `enable_request_coalescing(headers)` | Deduplicate concurrent identical requests | |
67 | 173 | |
68 | 174 | ## Database |
69 | 175 | |
70 | 176 | | Method | Description | |
71 | 177 | |---|---| |
72 | -| `set_database(url, init_sql, max_connections, request_acquire_timeout, wal, pragmas)` | Configure database connection pool | | |
178 | +| `set_database(url, ...)` | Configure database connection pool | | |
179 | +| `connect_database()` | Eagerly connect the pool | | |
180 | +| `db` (property) | Resolved `DbPool` handle | | |
181 | + | |
182 | +## Multi-Protocol | |
183 | + | |
184 | +| Method | Description | | |
185 | +|---|---| | |
186 | +| `graphql(path)` | Mount GraphQL endpoint | | |
187 | +| `set_grpc_addr(addr)` | Set gRPC listen address | | |
188 | +| `add_grpc_service(servicer, add_func)` | Register gRPC service | | |
73 | 189 | |
74 | -## Plugins | |
190 | +## Scheduling | |
75 | 191 | |
76 | 192 | | Method | Description | |
77 | 193 | |---|---| |
78 | -| `use(plugin)` | Register a plugin (calls `build()`, `on_startup()`, `on_shutdown()`) | | |
194 | +| `schedule(cron_expr, func)` | Register cron job (5-field UTC) | | |
195 | +| `every(seconds, func)` | Register interval job | | |
79 | 196 | |
80 | 197 | ## Agent System |
81 | 198 | |
82 | 199 | | Method | Description | |
83 | 200 | |---|---| |
84 | -| `tool` | Decorator to register an MCP tool | | |
85 | -| `stream_json(path, schema, mode)` | Decorator for validated JSON streaming | | |
86 | -| `enable_sessions()` | Enable Rust-backed agent session store | | |
87 | -| `enable_system_routes()` | Enable `/_system/tools` and `/_system/tools/call` | | |
88 | -| `run_mcp_stdio()` | Run MCP stdio server | | |
201 | +| `tool(func, name, description)` | Register MCP tool | | |
202 | +| `stream_json(path, schema, mode)` | Validated JSON streaming | | |
203 | +| `enable_sessions(cookie_name)` | Enable session store | | |
204 | +| `enable_system_routes()` | Enable `/_system/*` endpoints | | |
89 | 205 | |
90 | 206 | ## Observability |
91 | 207 | |
92 | 208 | | Method | Description | |
93 | 209 | |---|---| |
94 | -| `register_health_check(name, callable)` | Register a custom health check | | |
210 | +| `register_health_check(name, callable)` | Register custom health check | | |
211 | + | |
212 | +## Utilities | |
213 | + | |
214 | +| Method | Description | | |
215 | +|---|---| | |
216 | +| `url_for(name, **path_params)` | Build URL for named route | | |
217 | +| `frontend(path, directory)` | Serve static SPA | | |
218 | +| `use(plugin)` | Register a plugin | | |
95 | 219 | |
96 | 220 | ## Server Startup |
97 | 221 | |
98 | 222 | ```python |
99 | 223 | app.run( |
100 | 224 | addr="127.0.0.1:8000", |
101 | - max_body_size=50 * 1024 * 1024, # 50 MiB | |
225 | + max_body_size=50 * 1024 * 1024, | |
102 | 226 | ) |
103 | 227 | ``` |
104 | 228 | |
| @@ -107,49 +231,41 @@ app.run( | ||
107 | 231 | | `addr` | `str` | `"127.0.0.1:8000"` | Bind address | |
108 | 232 | | `max_body_size` | `int` | `52428800` | Max request body size (bytes) | |
109 | 233 | |
110 | -## Multi-Protocol | |
111 | - | |
112 | -| Method | Description | | |
113 | -|---|---| | |
114 | -| `graphql(path)` | Mount GraphQL endpoint with GraphiQL UI | | |
115 | - | |
116 | -## Exception Handlers | |
117 | - | |
118 | -| Method | Description | | |
119 | -|---|---| | |
120 | -| `exception_handler(exc_class)` | Decorator to register custom exception handler | | |
121 | - | |
122 | 234 | ## Example: Full Configuration |
123 | 235 | |
124 | 236 | ```python |
125 | -from justapi import JustAPIApp, Depends, HTTPException, Schema | |
126 | -from pydantic import Field | |
237 | +from justapi import JustAPIApp, Depends, HTTPException, Schema, APIRouter | |
127 | 238 | |
128 | 239 | app = JustAPIApp( |
129 | 240 | title="Enterprise API", |
130 | 241 | version="2.0.0", |
131 | - description="Production-grade API with all features", | |
242 | + servers=[{"url": "https://api.example.com"}], | |
243 | + contact={"name": "Support", "email": "[email protected]"}, | |
132 | 244 | ) |
133 | 245 | |
134 | -# Database | |
135 | -app.set_database("postgres://localhost/mydb") | |
246 | +app.state.cache = {} | |
136 | 247 | |
137 | -# Security | |
138 | -app.add_cors(allow_origins=["https://app.example.com"]) | |
139 | -app.enable_secure_headers(with_hsts=True) | |
140 | - | |
141 | -# Resilience | |
142 | -app.enable_circuit_breaker(failure_threshold=5, reset_timeout_ms=30000) | |
248 | +@app.on_event("startup") | |
249 | +def startup(): | |
250 | + app.state.db = connect() | |
143 | 251 | |
144 | -# Agent | |
145 | -app.enable_sessions() | |
146 | -app.enable_system_routes() | |
252 | +app.set_database("postgres://localhost/mydb") | |
253 | +app.add_cors(allow_origins=["https://app.example.com"]) | |
254 | +app.enable_circuit_breaker() | |
147 | 255 | |
148 | -# Routes | |
149 | -@app.get("/") | |
150 | -def root(request): | |
256 | +sub = APIRouter(prefix="/admin") | |
257 | +@sub.get("/health") | |
258 | +def health(request): | |
151 | 259 | return {"status": "ok"} |
152 | 260 | |
261 | +app.mount("/admin", sub) | |
262 | + | |
153 | 263 | if __name__ == "__main__": |
154 | - app.run("0.0.0.0:8080", max_body_size=1024 * 1024) | |
264 | + app.run("0.0.0.0:8080") | |
155 | 265 | ``` |
266 | + | |
267 | +## See Also | |
268 | + | |
269 | +- [Routing API](/api-reference/routing/) — Route decorator reference | |
270 | +- [APIRouter](/api-reference/apirouter/) — Modular route grouping | |
271 | +- [JustAPIApp Tutorial](/tutorials/routing-subrouters/) — Step-by-step guide | |
156 | 272 | |
▼docs_site/src/content/docs/api-reference/routing.md
+94-35
| @@ -1,7 +1,7 @@ | ||
1 | 1 | --- |
2 | 2 | title: Routing API |
3 | -description: "API reference for HTTP routing in JustAPI, the FastAPI alternative — method decorators and route registration." | |
4 | -keywords: [routing, fastapi alternative, justapi, http decorators, route registration, radix trie] | |
3 | +description: API reference for HTTP routing in JustAPI, the FastAPI alternative — method decorators, route registration, and programmatic add_api_route. | |
4 | +keywords: [routing, fastapi alternative, justapi, http decorators, route registration, radix trie, add_api_route, url_for] | |
5 | 5 | --- |
6 | 6 | |
7 | 7 | ## Method Decorators |
| @@ -13,8 +13,19 @@ Each HTTP method decorator registers a route handler in the radix-trie router. | ||
13 | 13 | ```python |
14 | 14 | @app.get( |
15 | 15 | path: str, |
16 | - dependencies: list | None = None, | |
16 | + dependencies: list[Depends] | None = None, | |
17 | + middlewares: list[Callable] | None = None, | |
17 | 18 | tags: list[str] | None = None, |
19 | + summary: str | None = None, | |
20 | + description: str | None = None, | |
21 | + deprecated: bool = False, | |
22 | + status_code: int | None = None, | |
23 | + responses: dict | None = None, | |
24 | + operation_id: str | None = None, | |
25 | + openapi_extra: dict | None = None, | |
26 | + name: str | None = None, | |
27 | + include_in_schema: bool = True, | |
28 | + native: bool = False, | |
18 | 29 | ) |
19 | 30 | ``` |
20 | 31 | |
| @@ -25,46 +36,70 @@ Each HTTP method decorator registers a route handler in the radix-trie router. | ||
25 | 36 | path: str, |
26 | 37 | body_schema: type[Schema] | None = None, |
27 | 38 | schema: type[Schema] | None = None, |
28 | - dependencies: list | None = None, | |
39 | + dependencies: list[Depends] | None = None, | |
40 | + middlewares: list[Callable] | None = None, | |
29 | 41 | tags: list[str] | None = None, |
42 | + summary: str | None = None, | |
43 | + description: str | None = None, | |
44 | + deprecated: bool = False, | |
45 | + status_code: int | None = None, | |
46 | + responses: dict | None = None, | |
47 | + operation_id: str | None = None, | |
48 | + openapi_extra: dict | None = None, | |
49 | + name: str | None = None, | |
50 | + include_in_schema: bool = True, | |
30 | 51 | native: bool = False, |
31 | 52 | ) |
32 | 53 | ``` |
33 | 54 | |
34 | -### `@app.put()` | |
55 | +### `@app.put()`, `@app.patch()` | |
56 | + | |
57 | +Same signature as `@app.post()`. | |
58 | + | |
59 | +### `@app.delete()`, `@app.head()`, `@app.options()`, `@app.trace()` | |
35 | 60 | |
36 | 61 | ```python |
37 | -@app.put( | |
62 | +@app.delete( | |
38 | 63 | path: str, |
39 | - body_schema: type[Schema] | None = None, | |
40 | - schema: type[Schema] | None = None, | |
41 | - dependencies: list | None = None, | |
42 | - tags: list[str] | None = None, | |
43 | - native: bool = False, | |
64 | + dependencies: list[Depends] | None = None, | |
65 | + ... | |
44 | 66 | ) |
45 | 67 | ``` |
46 | 68 | |
47 | -### `@app.patch()` | |
69 | +### `@app.websocket()` | |
48 | 70 | |
49 | 71 | ```python |
50 | -@app.patch( | |
51 | - path: str, | |
52 | - body_schema: type[Schema] | None = None, | |
53 | - schema: type[Schema] | None = None, | |
54 | - dependencies: list | None = None, | |
55 | - tags: list[str] | None = None, | |
56 | - native: bool = False, | |
57 | -) | |
72 | +@app.websocket(path: str) | |
58 | 73 | ``` |
59 | 74 | |
60 | -### `@app.delete()` | |
75 | +### `@app.sse()` | |
61 | 76 | |
62 | 77 | ```python |
63 | -@app.delete( | |
64 | - path: str, | |
65 | - dependencies: list | None = None, | |
66 | - tags: list[str] | None = None, | |
67 | -) | |
78 | +@app.sse(path: str) | |
79 | +``` | |
80 | + | |
81 | +### `@app.route()` | |
82 | + | |
83 | +Register a route that responds to multiple HTTP methods: | |
84 | + | |
85 | +```python | |
86 | +@app.route("/items", methods=["GET", "POST"]) | |
87 | +def items(request): | |
88 | + if request.method == "GET": | |
89 | + return list_items() | |
90 | + return create_item(request) | |
91 | +``` | |
92 | + | |
93 | +## Programmatic Route Registration | |
94 | + | |
95 | +Register routes without decorators (FastAPI parity): | |
96 | + | |
97 | +```python | |
98 | +def read_item(request, item_id: int): | |
99 | + return {"item_id": item_id} | |
100 | + | |
101 | +app.add_api_route("/items/{item_id}", read_item, methods=["GET"]) | |
102 | +app.add_api_websocket_route("/ws", my_ws_handler) | |
68 | 103 | ``` |
69 | 104 | |
70 | 105 | ## Route Parameters |
| @@ -73,9 +108,18 @@ Each HTTP method decorator registers a route handler in the radix-trie router. | ||
73 | 108 | |---|---|---|---| |
74 | 109 | | `path` | All | `str` | URL pattern with `{param}` placeholders | |
75 | 110 | | `body_schema` | POST, PUT, PATCH | `Schema` subclass | Validate body against this schema in Rust | |
76 | -| `schema` | POST, PUT, PATCH | `Schema` subclass | Alias for `body_schema` | | |
77 | 111 | | `dependencies` | All | `list[Depends]` | Route-level dependency injection | |
112 | +| `middlewares` | All | `list[Callable]` | Route-level middleware functions | | |
78 | 113 | | `tags` | All | `list[str]` | OpenAPI operation tags | |
114 | +| `summary` | All | `str` | OpenAPI operation summary | | |
115 | +| `description` | All | `str` | OpenAPI operation description | | |
116 | +| `deprecated` | All | `bool` | Mark deprecated in OpenAPI | | |
117 | +| `status_code` | All | `int` | Default response status code | | |
118 | +| `responses` | All | `dict` | Additional OpenAPI responses | | |
119 | +| `operation_id` | All | `str` | OpenAPI operation ID | | |
120 | +| `openapi_extra` | All | `dict` | Extra OpenAPI metadata | | |
121 | +| `include_in_schema` | All | `bool` | Exclude from OpenAPI schema | | |
122 | +| `name` | All | `str` | Name for `url_for()` | | |
79 | 123 | | `native` | POST, PUT, PATCH | `bool` | Execute entirely in Rust (724k+ RPS) | |
80 | 124 | |
81 | 125 | ## Path Parameter Types |
| @@ -89,14 +133,21 @@ Each HTTP method decorator registers a route handler in the radix-trie router. | ||
89 | 133 | | `UUID` | `/users/{uid}` | `550e8400-...` | |
90 | 134 | | `datetime` | `/events/{ts}` | `2026-07-24T12:00:00Z` | |
91 | 135 | |
92 | -## Native Fast Path | |
136 | +## Named Routes & URL Building | |
93 | 137 | |
94 | -When `native=True`, the handler executes entirely in Rust: | |
138 | +Use the `name` parameter and `url_for()` to build URLs: | |
95 | 139 | |
96 | -- Request body is validated against the schema in Rust | |
97 | -- The handler runs as a Rust callback | |
98 | -- The response is serialized in Rust | |
99 | -- Zero GIL acquisition for the entire request lifecycle | |
140 | +```python | |
141 | +@app.get("/items/{item_id}", name="get_item") | |
142 | +def read_item(request, item_id: int): | |
143 | + ... | |
144 | + | |
145 | +# Later: app.url_for("get_item", item_id=42) -> "/items/42" | |
146 | +``` | |
147 | + | |
148 | +## Native Fast Path | |
149 | + | |
150 | +When `native=True`, the handler validates the body and serializes the response entirely in Rust — zero GIL acquisition: | |
100 | 151 | |
101 | 152 | ```python |
102 | 153 | @app.post("/fast-items", body_schema=ItemSchema, native=True) |
| @@ -106,14 +157,22 @@ def create_item(request): | ||
106 | 157 | |
107 | 158 | ## Route Resolution |
108 | 159 | |
109 | -Routes are resolved in O(1) time using a radix-trie (matchit). The route-lookup cache memoizes repeated lookups for the same path. | |
160 | +Routes are resolved in O(1) time using a radix-trie (`matchit`). The route-lookup cache memoizes repeated lookups for the same path. | |
110 | 161 | |
111 | 162 | ## OpenAPI Generation |
112 | 163 | |
113 | -All registered routes automatically generate OpenAPI 3.1 documentation available at `/openapi.json`, `/docs` (Swagger UI), `/redoc` (ReDoc), and `/scalar` (Scalar UI). | |
164 | +All registered routes automatically generate OpenAPI 3.1 documentation: | |
165 | + | |
166 | +| URL | Description | | |
167 | +|---|---| | |
168 | +| `/docs` | Swagger UI | | |
169 | +| `/redoc` | ReDoc | | |
170 | +| `/scalar` | Scalar API Reference | | |
171 | +| `/openapi.json` | Raw OpenAPI 3.1 spec | | |
114 | 172 | |
115 | 173 | ## See Also |
116 | 174 | |
117 | 175 | - [APIRouter](/api-reference/apirouter/) — Modular route grouping |
118 | 176 | - [JustAPIApp](/api-reference/justapiapp/) — App configuration reference |
119 | 177 | - [Native Fast Path](/advanced/native-fast-path/) — Deep dive on Rust-native execution |
178 | +- [Routing & Sub-routers](/tutorials/routing-subrouters/) — Step-by-step tutorial | |
120 | 179 | |
▼docs_site/src/content/docs/tutorials/routing-subrouters.md
+67-47
| @@ -1,10 +1,10 @@ | ||
1 | 1 | --- |
2 | 2 | title: Routing & Sub-routers |
3 | -description: Organize your API with modular routers and route groups in JustAPI — a FastAPI alternative with Rust-powered routing. | |
4 | -keywords: routing, sub-routers, APIRouter, modular routes, JustAPI, FastAPI alternative, radix-trie | |
3 | +description: Organize your API with modular routers, sub-routers, and programmatic route registration in JustAPI — a FastAPI alternative with Rust-powered routing. | |
4 | +keywords: routing, sub-routers, APIRouter, include_router, modular routes, JustAPI, FastAPI alternative, add_api_route, url_for, named routes | |
5 | 5 | --- |
6 | 6 | |
7 | -JustAPI's routing system uses a radix-trie (via `matchit`) for O(1) route matching. Routes are registered with decorators and can be organized across multiple files using `APIRouter`. | |
7 | +JustAPI's routing system uses a radix-trie (via `matchit`) for O(1) route matching. Routes can be registered with decorators, programmatically, or organized across multiple files using `APIRouter`. | |
8 | 8 | |
9 | 9 | ## HTTP Method Decorators |
10 | 10 | |
| @@ -13,32 +13,31 @@ from justapi import JustAPIApp | ||
13 | 13 | |
14 | 14 | app = JustAPIApp() |
15 | 15 | |
16 | - | |
17 | 16 | @app.get("/users") |
18 | 17 | def list_users(request): |
19 | 18 | return [{"id": 1, "name": "Alice"}] |
20 | 19 | |
21 | - | |
22 | 20 | @app.post("/users") |
23 | 21 | def create_user(request): |
24 | 22 | return {"message": "User created"} |
23 | +``` | |
25 | 24 | |
25 | +All standard HTTP methods are supported: `@app.get()`, `@app.post()`, `@app.put()`, `@app.patch()`, `@app.delete()`, `@app.head()`, `@app.options()`, `@app.trace()`, and `@app.websocket()`. | |
26 | 26 | |
27 | -@app.put("/users/{user_id}") | |
28 | -def update_user(request, user_id: int): | |
29 | - return {"user_id": user_id, "updated": True} | |
30 | - | |
27 | +## Programmatic Route Registration (Non-Decorator) | |
31 | 28 | |
32 | -@app.patch("/users/{user_id}") | |
33 | -def patch_user(request, user_id: int): | |
34 | - return {"user_id": user_id, "patched": True} | |
29 | +Register routes without decorators using `add_api_route()`: | |
35 | 30 | |
31 | +```python | |
32 | +def read_item(request, item_id: int): | |
33 | + return {"item_id": item_id, "name": "Item"} | |
36 | 34 | |
37 | -@app.delete("/users/{user_id}") | |
38 | -def delete_user(request, user_id: int): | |
39 | - return {"deleted": user_id} | |
35 | +app.add_api_route("/items/{item_id}", read_item, methods=["GET"]) | |
36 | +app.add_api_websocket_route("/ws", my_ws_handler) | |
40 | 37 | ``` |
41 | 38 | |
39 | +This is useful when importing handler functions from other modules or when routes need to be registered conditionally. | |
40 | + | |
42 | 41 | ## Using `APIRouter` for Modular Routes |
43 | 42 | |
44 | 43 | Group related routes in separate files: |
| @@ -49,91 +48,111 @@ from justapi import APIRouter | ||
49 | 48 | |
50 | 49 | router = APIRouter(prefix="/products", tags=["Products"]) |
51 | 50 | |
52 | - | |
53 | 51 | @router.get("/") |
54 | 52 | def list_products(request): |
55 | 53 | return [{"id": 1, "name": "Rust Book"}] |
56 | 54 | |
57 | - | |
58 | 55 | @router.get("/{product_id}") |
59 | 56 | def get_product(request, product_id: int): |
60 | - return {"product_id": product_id, "name": f"Product {product_id}"} | |
57 | + return {"product_id": product_id} | |
61 | 58 | ``` |
62 | 59 | |
63 | 60 | ```python |
64 | -# app/routers/reviews.py | |
61 | +# app/main.py | |
62 | +from justapi import JustAPIApp | |
63 | +from app.routers.products import router as products_router | |
64 | + | |
65 | +app = JustAPIApp() | |
66 | +app.include_router(products_router, prefix="/api/v1") | |
67 | +``` | |
68 | + | |
69 | +## Sub-Routers (Nested Routers) | |
70 | + | |
71 | +APIRouter instances can include other APIRouter instances, creating nested route hierarchies: | |
72 | + | |
73 | +```python | |
65 | 74 | from justapi import APIRouter |
66 | 75 | |
67 | -router = APIRouter(prefix="/products/{product_id}/reviews", tags=["Reviews"]) | |
76 | +# Create a sub-router for user-related routes | |
77 | +users_router = APIRouter(prefix="/users") | |
78 | + | |
79 | +@users_router.get("/{user_id}") | |
80 | +def get_user(request, user_id: int): | |
81 | + return {"user_id": user_id} | |
68 | 82 | |
83 | +@users_router.get("/{user_id}/orders") | |
84 | +def get_user_orders(request, user_id: int): | |
85 | + return [{"order_id": 1, "user_id": user_id}] | |
69 | 86 | |
70 | -@router.get("/") | |
71 | -def list_reviews(request, product_id: int): | |
72 | - return [{"id": 1, "product_id": product_id, "rating": 5}] | |
87 | +# Create an admin router and include the users sub-router | |
88 | +admin_router = APIRouter(prefix="/admin", tags=["Admin"]) | |
89 | +admin_router.include_router(users_router) | |
90 | + | |
91 | +# Include the admin router in the app | |
92 | +app.include_router(admin_router, prefix="/api/v1") | |
93 | +# Final URL: /api/v1/admin/users/{user_id} | |
73 | 94 | ``` |
74 | 95 | |
96 | +## Sub-Application Mounting | |
97 | + | |
98 | +Use `app.mount()` to mount APIRouters or static directories: | |
99 | + | |
75 | 100 | ```python |
76 | -# app/main.py | |
77 | -from justapi import JustAPIApp | |
78 | -from app.routers.products import router as products_router | |
79 | -from app.routers.reviews import router as reviews_router | |
101 | +# Mount an APIRouter as a sub-app | |
102 | +app.mount("/api/v1", users_router) | |
80 | 103 | |
81 | -app = JustAPIApp() | |
82 | -app.include_router(products_router) | |
83 | -app.include_router(reviews_router) | |
104 | +# Mount a static directory | |
105 | +app.mount("/static", "static", name="static") | |
84 | 106 | ``` |
85 | 107 | |
86 | -## Route with Tags | |
108 | +## Named Routes & URL Building | |
87 | 109 | |
88 | -Tags group endpoints in the auto-generated OpenAPI documentation: | |
110 | +Use the `name` parameter and `url_for()` to build URLs dynamically: | |
89 | 111 | |
90 | 112 | ```python |
91 | -@router.get("/", tags=["Products"]) | |
92 | -def list_products(request): | |
113 | +@app.get("/items/{item_id}", name="get_item") | |
114 | +def read_item(request, item_id: int): | |
93 | 115 | ... |
116 | + | |
117 | +# Build URL for a named route | |
118 | +url = app.url_for("get_item", item_id=42) | |
119 | +# Returns: "/items/42" | |
94 | 120 | ``` |
95 | 121 | |
96 | -## Router-Level Middleware | |
122 | +Named routes work with APIRouter too, and URLs are correctly resolved even with nested prefixes. | |
123 | + | |
124 | +## Router-Level Dependencies | |
97 | 125 | |
98 | 126 | ```python |
99 | 127 | from justapi import APIRouter, Depends, HTTPException, Header |
100 | 128 | |
101 | 129 | admin_router = APIRouter(prefix="/admin", tags=["Admin"]) |
102 | 130 | |
103 | - | |
104 | 131 | def require_admin(authorization: str = Header(...)): |
105 | 132 | if authorization != "Bearer admin-token": |
106 | 133 | raise HTTPException(403, "Admin access required") |
107 | 134 | |
108 | - | |
109 | 135 | @admin_router.get("/users", dependencies=[Depends(require_admin)]) |
110 | 136 | def list_all_users(request): |
111 | 137 | return [{"id": 1, "name": "Alice"}] |
112 | 138 | |
113 | - | |
114 | 139 | app.include_router(admin_router) |
115 | 140 | ``` |
116 | 141 | |
117 | 142 | ## Route Ordering |
118 | 143 | |
119 | -Routes are matched in the order they are registered. More specific routes should come before parameterized routes: | |
144 | +Routes are matched in order of registration. Specific routes before parameterized: | |
120 | 145 | |
121 | 146 | ```python |
122 | -# Specific route first | |
123 | -@app.get("/users/me") | |
147 | +@app.get("/users/me") # Specific route first | |
124 | 148 | def get_current_user(request): |
125 | 149 | return {"user": "current"} |
126 | 150 | |
127 | -# Parameterized route second | |
128 | -@app.get("/users/{user_id}") | |
151 | +@app.get("/users/{user_id}") # Parameterized route second | |
129 | 152 | def get_user(request, user_id: int): |
130 | 153 | return {"user_id": user_id} |
131 | 154 | ``` |
132 | 155 | |
133 | -## Route-Lookup Cache | |
134 | - | |
135 | -For high-traffic routes, JustAPI caches route lookups in a memoization table (ADR-064). This avoids radix-trie traversal on repeated requests to the same path. | |
136 | - | |
137 | 156 | ## OpenAPI Documentation |
138 | 157 | |
139 | 158 | All registered routes are automatically documented: |
| @@ -149,4 +168,5 @@ All registered routes are automatically documented: | ||
149 | 168 | |
150 | 169 | - [API Reference: Routing](/api-reference/routing/) — Complete routing API |
151 | 170 | - [API Reference: APIRouter](/api-reference/apirouter/) — Router reference |
171 | +- [API Reference: JustAPIApp](/api-reference/justapiapp/) — App configuration | |
152 | 172 | - [Multi-Protocol APIs](/advanced/multi-protocol-apis/) — REST, GraphQL, gRPC, JSON-RPC |
153 | 173 | |