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
▼crates/justapi-py/python/justapi/app.py
+116-3
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
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
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
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
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
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
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
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
|---|---|---|---|
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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