Skip to content

docs(event_handler): improve routing rules syntax documentation #7094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
65 changes: 49 additions & 16 deletions docs/core/event_handler/api_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ You can use `/todos/<todo_id>` to configure dynamic URL paths, where `<todo_id>`

Each dynamic route you set must be part of your function signature. This allows us to call your function using keyword arguments when matching your dynamic route.

???+ note
For brevity, we will only include the necessary keys for each sample request for the example to work.
???+ tip
You can also nest dynamic paths, for example `/todos/<todo_id>/<todo_status>`.

=== "dynamic_routes.py"

Expand All @@ -211,32 +211,65 @@ Each dynamic route you set must be part of your function signature. This allows
--8<-- "examples/event_handler_rest/src/dynamic_routes.json"
```

???+ tip
You can also nest dynamic paths, for example `/todos/<todo_id>/<todo_status>`.
#### Dynamic path mechanism

Dynamic path parameters are defined using angle brackets `<parameter_name>` syntax. These parameters are automatically converted to regex patterns for efficient route matching and performance gains.

**Syntax**: `/path/<parameter_name>`

* **Parameter names** must contain only word characters (letters, numbers, underscore)
* **Captured values** can contain letters, numbers, underscores, and these special characters: `-._~()'!*:@,;=+&$%<> \[]{}|^`. Reserved characters must be percent-encoded in URLs to prevent errors.

| Route Pattern | Matches | Doesn't Match |
|---------------|---------|---------------|
| `/users/<user_id>` | `/users/123`, `/users/user-456` | `/users/123/profile` |
| `/api/<version>/users` | `/api/v1/users`, `/api/2.0/users` | `/api/users` |
| `/files/<path>` | `/files/document.pdf`, `/files/folder%20name` | `/files/sub/folder/file.txt` |
| `/files/<folder>/<name>` | `/files/src/document.pdf`, `/files/src/test.txt` | `/files/sub/folder/file.txt` |

=== "routing_syntax_basic.py"

```python hl_lines="11 18"
--8<-- "examples/event_handler_rest/src/routing_syntax_basic.py"
```

=== "routing_advanced_examples.py"

```python hl_lines="11 22"
--8<-- "examples/event_handler_rest/src/routing_advanced_examples.py"
```

???+ tip "Function parameter names must match"
The parameter names in your route (`<user_id>`) must exactly match the parameter names in your function signature (`user_id: str`). This is how the framework knows which captured values to pass to which parameters.

#### Catch-all routes

???+ note
We recommend having explicit routes whenever possible; use catch-all routes sparingly.
For scenarios where you need to handle arbitrary or deeply nested paths, you can use regex patterns directly in your route definitions. These are particularly useful for proxy routes or when dealing with file paths.

You can use a [regex](https://docs.python.org/3/library/re.html#regular-expression-syntax){target="_blank" rel="nofollow"} string to handle an arbitrary number of paths within a request, for example `.+`.
**We recommend** having explicit routes whenever possible; use catch-all routes sparingly.

You can also combine nested paths with greedy regex to catch in between routes.
##### Using Regex Patterns

???+ warning
We choose the most explicit registered route that matches an incoming event.
You can use standard [Python regex patterns](https://docs.python.org/3/library/re.html#regular-expression-syntax){target="_blank" rel="nofollow"} in your route definitions, for example:

| Pattern | Description | Examples |
|---------|-------------|----------|
| `.+` | Matches one or more characters (greedy) | `/proxy/.+` matches `/proxy/any/deep/path` |
| `.*` | Matches zero or more characters (greedy) | `/files/.*` matches `/files/` and `/files/deep/path` |
| `[^/]+` | Matches one or more non-slash characters | `/api/[^/]+` matches `/api/v1` but not `/api/v1/users` |
| `\w+` | Matches one or more word characters | `/users/\w+` matches `/users/john123` |

=== "dynamic_routes_catch_all.py"

```python hl_lines="11"
```python hl_lines="11 17 18 24 25 30 31 36 37"
--8<-- "examples/event_handler_rest/src/dynamic_routes_catch_all.py"
```

=== "dynamic_routes_catch_all.json"

```json
--8<-- "examples/event_handler_rest/src/dynamic_routes_catch_all.json"
```
???+ warning "Route Matching Priority"
- Routes are matched in **order of specificity**, not registration order
- More specific routes (exact matches) take precedence over regex patterns
- Among regex routes, the first registered matching route wins
- Always place catch-all routes (`.*`) last

### HTTP Methods

Expand Down
26 changes: 26 additions & 0 deletions examples/event_handler_rest/src/dynamic_routes_catch_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@ def catch_any_route_get_method():
return {"path_received": app.current_event.path}


# File path proxy - captures everything after /files/
@app.get("/files/.+")
def serve_file():
file_path = app.current_event.path.replace("/files/", "")
return {"file_path": file_path}


# API versioning with any format
@app.get(r"/api/v\d+/.*") # Matches /api/v1/users, /api/v2/posts/123
def handle_versioned_api():
return {"api_version": "handled"}


# Catch-all for unmatched routes
@app.route(".*", method=["GET", "POST"]) # Must be last route
def catch_all():
return {"message": "Route not found", "path": app.current_event.path}


# Mixed: dynamic parameter + regex catch-all
@app.get("/users/<user_id>/files/.+")
def get_user_files(user_id: str):
file_path = app.current_event.path.split(f"/users/{user_id}/files/")[1]
return {"user_id": user_id, "file_path": file_path}


# You can continue to use other utilities just as before
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
Expand Down
32 changes: 32 additions & 0 deletions examples/event_handler_rest/src/routing_advanced_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext

tracer = Tracer()
logger = Logger()
app = APIGatewayRestResolver()


@app.get("/api/<api_version>/resources/<resource_type>/<resource_id>")
@tracer.capture_method
def get_resource(api_version: str, resource_type: str, resource_id: str):
# handles nested dynamic parameters in API versioned routes
return {
"version": api_version,
"type": resource_type,
"id": resource_id,
}


@app.get("/organizations/<org_id>/teams/<team_id>/members")
@tracer.capture_method
def list_team_members(org_id: str, team_id: str):
# combines dynamic paths with static segments
return {"org": org_id, "team": team_id, "action": "list_members"}


@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)
28 changes: 28 additions & 0 deletions examples/event_handler_rest/src/routing_syntax_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext

tracer = Tracer()
logger = Logger()
app = APIGatewayRestResolver()


@app.get("/users/<user_id>")
@tracer.capture_method
def get_user(user_id: str):
# user_id value comes as a string with special chars support
return {"user_id": user_id}


@app.get("/orders/<order_id>/items/<item_id>")
@tracer.capture_method
def get_order_item(order_id: str, item_id: str):
# multiple dynamic parameters are supported
return {"order_id": order_id, "item_id": item_id}


@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)