Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,6 @@ Use `requests` library for HTTP assertions in tests.

- The project uses `ahash` instead of standard `HashMap` for performance
- Path parameters in routes use `{param_name}` syntax
- Middleware applies to all routes in the same scope; use `.scope()` to create new middleware groups
- Middleware applies to all routes registered **after** it within the same router; use separate `Router` instances to isolate middleware groups
- Multiple routers can be attached to the server and are checked in order until a matching route is found
- Application state is shared via `request.app_data`
124 changes: 85 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,56 +90,102 @@ if __name__ == "__main__":
asyncio.run(main())
```

## Middleware Example
## Middleware

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.
OxAPY offers two paradigms for organizing middleware. You can use one or combine both.

### Best Practices
### 1. Sequence Paradigm (same router)

- **Order Matters**: Middleware is executed in the order it is defined within a scope.
- **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.
- **Clarity**: Be mindful that middleware applies to all routes defined in the current scope, both before and after the middleware is added.
Middleware only applies to routes registered **after** it within the same router. Routes before it get no middleware.

```python
from oxapy import Status, Router, get, HttpServer
# Simple: one middleware layer
Router()
.route(get("/health", lambda _: "OK")) # no middleware
.middleware(auth)
.route(get("/dashboard", dashboard)) # auth only
.route(get("/account", account)) # auth only
```

def log_middleware(request, next, **kwargs):
print(f"Request: {request.method} {request.uri}")
return next(request, **kwargs)
```python
# Multiple layers: each middleware applies to everything after it
Router()
.route(get("/health", lambda _: "OK")) # no middleware
.route(static_file()) # no middleware
.middleware(session)
.route(get("/login", login)) # session
.route(get("/register", register)) # session
.middleware(db_session)
.route(get("/search", search)) # session + db_session
.route(get("/profile", profile)) # session + db_session
.middleware(protect_page)
.route(get("/admin", admin)) # session + db_session + protect_page
```

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

@get("/public")
def public(request):
return "This is a public route."
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.

@get("/protected")
def protected(request):
return "This is a protected route."
```python
# Simple: two isolated groups
HttpServer(("127.0.0.1", 5555))
.attach(
Router()
.route(get("/health", lambda _: "OK"))
.route(static_file())
)
.attach(
Router()
.middleware(auth)
.route(get("/dashboard", dashboard))
.route(get("/account", account))
)
```

def main():
(
HttpServer(("127.0.0.1", 5555))
.attach(
Router()
# First scope: public routes with logging
.route(public)
.middleware(log_middleware)

# Second scope: protected routes with logging and authentication
.scope()
.route(protected)
.middleware(log_middleware)
.middleware(auth_middleware)
)
.run()
)
```python
# Multiple isolated groups with different middleware stacks
HttpServer(("127.0.0.1", 5555))
.attach(
Router()
.route(static_file())
.route(get("/health", lambda _: "Good!"))
)
.attach(
Router()
.middleware(session)
.middleware(db_session)
.routes([login_user, register_user, show_login_page])
)
.attach(
Router()
.middleware(session)
.middleware(db_session)
.middleware(protect_page)
.routes([show_dashboard, show_account, logout_user])
)
```

if __name__ == "__main__":
main()
### 3. Combined (both paradigms)

Use sequence layering inside a router alongside separate routers.

```python
HttpServer(("127.0.0.1", 5555))
.attach(
Router()
.route(get("/health", lambda _: "OK")) # no middleware
.middleware(rate_limit)
.route(get("/login", login)) # rate_limit
.route(get("/register", register)) # rate_limit
)
.attach(
Router()
.middleware(session)
.middleware(db_session)
.route(get("/dashboard", dashboard)) # session + db_session
.middleware(protect_page)
.route(get("/admin", admin)) # session + db_session + protect_page
)
```

## Static Files
Expand Down
93 changes: 24 additions & 69 deletions oxapy/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,9 @@ class HttpServer:
r"""
Attach a router to the server.

Multiple routers can be attached and are checked in order until a matching route is found.
This is the recommended way to group routes with different middleware.

Args:
router (Router): The router instance to attach.

Expand All @@ -470,24 +473,20 @@ class HttpServer:
```python
from oxapy import Router, get, post

# Define a simple hello world handler
@get("/")
def hello(request):
return "Hello, World!"

# Handler with path parameters
@get("/users/{user_id}")
def get_user(request, user_id: int):
return f"User ID: {user_id}"

# Handler that returns JSON
@post("/api/data")
def get_data(request):
return {"message": "Success", "data": [1, 2, 3]}

router = Router()
router.routes([hello, get_user, get_data])
# Attach the router to the server
server.attach(router)
```
"""
Expand Down Expand Up @@ -1010,6 +1009,10 @@ class Router:
The Router is responsible for registering routes and handling HTTP requests.
It supports path parameters, middleware, and different HTTP methods.

Middleware applies to all routes registered **after** it within the same router.
To isolate middleware to specific groups, create separate `Router` instances
and attach each one to the server.

A `base_path` can be provided to prepend a path to all routes.

Returns:
Expand Down Expand Up @@ -1048,51 +1051,37 @@ class Router:

# Router without a base path
router = Router()

# To isolate middleware groups, create separate routers:
public_routes = Router()
protected_routes = Router()
server.attach(public_routes).attach(protected_routes)
```
"""
def middleware(self, middleware: typing.Any) -> Router:
r"""
Add middleware to the current routing layer.
Add a middleware to the router.

Middleware is applied to all routes defined in the current layer (scope).
To create a new layer with a separate set of middleware, use the `.scope()` method.
Middleware only applies to routes registered **after** it within the same router.
This lets you layer middleware naturally by registration order.
Use separate `Router` instances for full middleware isolation across groups.
Middleware functions are executed in the order they are added.

Args:
middleware (callable): A function that will process requests before route handlers in the current layer.
middleware (callable): A function that will process requests before route handlers.

Returns:
Router: The router instance, allowing for method chaining.

Example:
```python
from oxapy import Status, Router, get

def log_middleware(request, next, **kwargs):
print(f"Request: {request.method} {request.path}")
return next(request, **kwargs)

def auth_middleware(request, next, **kwargs):
if "authorization" not in request.headers:
return Status.UNAUTHORIZED
return next(request, **kwargs)

router = (
Router()
# Scope 1: public routes with logging
.route(get("/status", lambda r: "OK"))
.middleware(log_middleware)

# Scope 2: protected routes with logging and auth
.scope()
.route(get("/admin", lambda r: "Admin Area"))
.middleware(log_middleware)
router = Router()
.route(get("/health", lambda _: "OK"))
.middleware(session_middleware)
.middleware(db_middleware)
.route(get("/login", login))
.middleware(auth_middleware)
)

# In this example:
# - Requests to /status will go through log_middleware.
# - Requests to /admin will go through log_middleware and then auth_middleware.
# - The middleware from the first scope does not affect the second scope.
.route(get("/dashboard", dashboard))
```
"""
def route(self, route: Route) -> Router:
Expand Down Expand Up @@ -1149,40 +1138,6 @@ class Router:
router.routes(routes)
```
"""
def scope(self) -> Router:
r"""
Create a new routing layer (scope).

Scopes are used to group routes with a specific set of middleware.
Middleware applied to a scope will only affect routes defined within that scope.

Returns:
Router: The router instance, allowing for method chaining.

Example:
```python
from oxapy import Router, get

def middleware_a(request, next, **kwargs):
print("Middleware A")
return next(request, **kwargs)

def middleware_b(request, next, **kwargs):
print("Middleware B")
return next(request, **kwargs)

router = (
Router()
.route(get("/route1", lambda r: "Route 1"))
.middleware(middleware_a)
.scope()
.route(get("/route2", lambda r: "Route 2"))
.middleware(middleware_b)
)
# /route1 is affected by middleware_a.
# /route2 is affected by middleware_b, but not middleware_a.
```
"""
def __repr__(self) -> builtins.str: ...

@typing.final
Expand Down
Loading
Loading