Skip to content

Commit d1df957

Browse files
authored
feat: organize route & middleware by sequence (#87)
* feat: sequence matter * chore: move redirect to import * chore: update docstring
1 parent 6e862f8 commit d1df957

8 files changed

Lines changed: 200 additions & 244 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,5 +177,6 @@ Use `requests` library for HTTP assertions in tests.
177177

178178
- The project uses `ahash` instead of standard `HashMap` for performance
179179
- Path parameters in routes use `{param_name}` syntax
180-
- Middleware applies to all routes in the same scope; use `.scope()` to create new middleware groups
180+
- Middleware applies to all routes registered **after** it within the same router; use separate `Router` instances to isolate middleware groups
181+
- Multiple routers can be attached to the server and are checked in order until a matching route is found
181182
- Application state is shared via `request.app_data`

README.md

Lines changed: 85 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -90,56 +90,102 @@ if __name__ == "__main__":
9090
asyncio.run(main())
9191
```
9292

93-
## Middleware Example
93+
## Middleware
9494

95-
OxAPY's middleware system is designed to be flexible and powerful. Middleware is applied to all routes within the same **scope**. You can use the `.scope()` method to create new scopes, allowing you to group routes with different middleware. This allows for building complex routing structures where different sets of middleware apply to different groups of routes.
95+
OxAPY offers two paradigms for organizing middleware. You can use one or combine both.
9696

97-
### Best Practices
97+
### 1. Sequence Paradigm (same router)
9898

99-
- **Order Matters**: Middleware is executed in the order it is defined within a scope.
100-
- **Scoping**: Use `.scope()` to create logical separation between groups of routes and their middleware. For example, you can have one scope for public endpoints and another for authenticated endpoints.
101-
- **Clarity**: Be mindful that middleware applies to all routes defined in the current scope, both before and after the middleware is added.
99+
Middleware only applies to routes registered **after** it within the same router. Routes before it get no middleware.
102100

103101
```python
104-
from oxapy import Status, Router, get, HttpServer
102+
# Simple: one middleware layer
103+
Router()
104+
.route(get("/health", lambda _: "OK")) # no middleware
105+
.middleware(auth)
106+
.route(get("/dashboard", dashboard)) # auth only
107+
.route(get("/account", account)) # auth only
108+
```
105109

106-
def log_middleware(request, next, **kwargs):
107-
print(f"Request: {request.method} {request.uri}")
108-
return next(request, **kwargs)
110+
```python
111+
# Multiple layers: each middleware applies to everything after it
112+
Router()
113+
.route(get("/health", lambda _: "OK")) # no middleware
114+
.route(static_file()) # no middleware
115+
.middleware(session)
116+
.route(get("/login", login)) # session
117+
.route(get("/register", register)) # session
118+
.middleware(db_session)
119+
.route(get("/search", search)) # session + db_session
120+
.route(get("/profile", profile)) # session + db_session
121+
.middleware(protect_page)
122+
.route(get("/admin", admin)) # session + db_session + protect_page
123+
```
109124

110-
def auth_middleware(request, next, **kwargs):
111-
if "authorization" not in request.headers:
112-
return Status.UNAUTHORIZED
113-
return next(request, **kwargs)
125+
### 2. Multi-Router Paradigm (separate routers)
114126

115-
@get("/public")
116-
def public(request):
117-
return "This is a public route."
127+
Each router has its own independent middleware stack. Routers are checked in order until a match is found. Use this when groups share no middleware.
118128

119-
@get("/protected")
120-
def protected(request):
121-
return "This is a protected route."
129+
```python
130+
# Simple: two isolated groups
131+
HttpServer(("127.0.0.1", 5555))
132+
.attach(
133+
Router()
134+
.route(get("/health", lambda _: "OK"))
135+
.route(static_file())
136+
)
137+
.attach(
138+
Router()
139+
.middleware(auth)
140+
.route(get("/dashboard", dashboard))
141+
.route(get("/account", account))
142+
)
143+
```
122144

123-
def main():
124-
(
125-
HttpServer(("127.0.0.1", 5555))
126-
.attach(
127-
Router()
128-
# First scope: public routes with logging
129-
.route(public)
130-
.middleware(log_middleware)
131-
132-
# Second scope: protected routes with logging and authentication
133-
.scope()
134-
.route(protected)
135-
.middleware(log_middleware)
136-
.middleware(auth_middleware)
137-
)
138-
.run()
139-
)
145+
```python
146+
# Multiple isolated groups with different middleware stacks
147+
HttpServer(("127.0.0.1", 5555))
148+
.attach(
149+
Router()
150+
.route(static_file())
151+
.route(get("/health", lambda _: "Good!"))
152+
)
153+
.attach(
154+
Router()
155+
.middleware(session)
156+
.middleware(db_session)
157+
.routes([login_user, register_user, show_login_page])
158+
)
159+
.attach(
160+
Router()
161+
.middleware(session)
162+
.middleware(db_session)
163+
.middleware(protect_page)
164+
.routes([show_dashboard, show_account, logout_user])
165+
)
166+
```
140167

141-
if __name__ == "__main__":
142-
main()
168+
### 3. Combined (both paradigms)
169+
170+
Use sequence layering inside a router alongside separate routers.
171+
172+
```python
173+
HttpServer(("127.0.0.1", 5555))
174+
.attach(
175+
Router()
176+
.route(get("/health", lambda _: "OK")) # no middleware
177+
.middleware(rate_limit)
178+
.route(get("/login", login)) # rate_limit
179+
.route(get("/register", register)) # rate_limit
180+
)
181+
.attach(
182+
Router()
183+
.middleware(session)
184+
.middleware(db_session)
185+
.route(get("/dashboard", dashboard)) # session + db_session
186+
.middleware(protect_page)
187+
.route(get("/admin", admin)) # session + db_session + protect_page
188+
)
143189
```
144190

145191
## Static Files

oxapy/__init__.pyi

Lines changed: 24 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,9 @@ class HttpServer:
460460
r"""
461461
Attach a router to the server.
462462
463+
Multiple routers can be attached and are checked in order until a matching route is found.
464+
This is the recommended way to group routes with different middleware.
465+
463466
Args:
464467
router (Router): The router instance to attach.
465468
@@ -470,24 +473,20 @@ class HttpServer:
470473
```python
471474
from oxapy import Router, get, post
472475
473-
# Define a simple hello world handler
474476
@get("/")
475477
def hello(request):
476478
return "Hello, World!"
477479
478-
# Handler with path parameters
479480
@get("/users/{user_id}")
480481
def get_user(request, user_id: int):
481482
return f"User ID: {user_id}"
482483
483-
# Handler that returns JSON
484484
@post("/api/data")
485485
def get_data(request):
486486
return {"message": "Success", "data": [1, 2, 3]}
487487
488488
router = Router()
489489
router.routes([hello, get_user, get_data])
490-
# Attach the router to the server
491490
server.attach(router)
492491
```
493492
"""
@@ -1010,6 +1009,10 @@ class Router:
10101009
The Router is responsible for registering routes and handling HTTP requests.
10111010
It supports path parameters, middleware, and different HTTP methods.
10121011
1012+
Middleware applies to all routes registered **after** it within the same router.
1013+
To isolate middleware to specific groups, create separate `Router` instances
1014+
and attach each one to the server.
1015+
10131016
A `base_path` can be provided to prepend a path to all routes.
10141017
10151018
Returns:
@@ -1048,51 +1051,37 @@ class Router:
10481051
10491052
# Router without a base path
10501053
router = Router()
1054+
1055+
# To isolate middleware groups, create separate routers:
1056+
public_routes = Router()
1057+
protected_routes = Router()
1058+
server.attach(public_routes).attach(protected_routes)
1059+
```
10511060
"""
10521061
def middleware(self, middleware: typing.Any) -> Router:
10531062
r"""
1054-
Add middleware to the current routing layer.
1063+
Add a middleware to the router.
10551064
1056-
Middleware is applied to all routes defined in the current layer (scope).
1057-
To create a new layer with a separate set of middleware, use the `.scope()` method.
1065+
Middleware only applies to routes registered **after** it within the same router.
1066+
This lets you layer middleware naturally by registration order.
1067+
Use separate `Router` instances for full middleware isolation across groups.
10581068
Middleware functions are executed in the order they are added.
10591069
10601070
Args:
1061-
middleware (callable): A function that will process requests before route handlers in the current layer.
1071+
middleware (callable): A function that will process requests before route handlers.
10621072
10631073
Returns:
10641074
Router: The router instance, allowing for method chaining.
10651075
10661076
Example:
10671077
```python
1068-
from oxapy import Status, Router, get
1069-
1070-
def log_middleware(request, next, **kwargs):
1071-
print(f"Request: {request.method} {request.path}")
1072-
return next(request, **kwargs)
1073-
1074-
def auth_middleware(request, next, **kwargs):
1075-
if "authorization" not in request.headers:
1076-
return Status.UNAUTHORIZED
1077-
return next(request, **kwargs)
1078-
1079-
router = (
1080-
Router()
1081-
# Scope 1: public routes with logging
1082-
.route(get("/status", lambda r: "OK"))
1083-
.middleware(log_middleware)
1084-
1085-
# Scope 2: protected routes with logging and auth
1086-
.scope()
1087-
.route(get("/admin", lambda r: "Admin Area"))
1088-
.middleware(log_middleware)
1078+
router = Router()
1079+
.route(get("/health", lambda _: "OK"))
1080+
.middleware(session_middleware)
1081+
.middleware(db_middleware)
1082+
.route(get("/login", login))
10891083
.middleware(auth_middleware)
1090-
)
1091-
1092-
# In this example:
1093-
# - Requests to /status will go through log_middleware.
1094-
# - Requests to /admin will go through log_middleware and then auth_middleware.
1095-
# - The middleware from the first scope does not affect the second scope.
1084+
.route(get("/dashboard", dashboard))
10961085
```
10971086
"""
10981087
def route(self, route: Route) -> Router:
@@ -1149,40 +1138,6 @@ class Router:
11491138
router.routes(routes)
11501139
```
11511140
"""
1152-
def scope(self) -> Router:
1153-
r"""
1154-
Create a new routing layer (scope).
1155-
1156-
Scopes are used to group routes with a specific set of middleware.
1157-
Middleware applied to a scope will only affect routes defined within that scope.
1158-
1159-
Returns:
1160-
Router: The router instance, allowing for method chaining.
1161-
1162-
Example:
1163-
```python
1164-
from oxapy import Router, get
1165-
1166-
def middleware_a(request, next, **kwargs):
1167-
print("Middleware A")
1168-
return next(request, **kwargs)
1169-
1170-
def middleware_b(request, next, **kwargs):
1171-
print("Middleware B")
1172-
return next(request, **kwargs)
1173-
1174-
router = (
1175-
Router()
1176-
.route(get("/route1", lambda r: "Route 1"))
1177-
.middleware(middleware_a)
1178-
.scope()
1179-
.route(get("/route2", lambda r: "Route 2"))
1180-
.middleware(middleware_b)
1181-
)
1182-
# /route1 is affected by middleware_a.
1183-
# /route2 is affected by middleware_b, but not middleware_a.
1184-
```
1185-
"""
11861141
def __repr__(self) -> builtins.str: ...
11871142

11881143
@typing.final

0 commit comments

Comments
 (0)