Skip to content

docs(api-gateway): add support for new router feature #767

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
merged 18 commits into from
Nov 12, 2021
Merged
Changes from 15 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
270 changes: 264 additions & 6 deletions docs/core/event_handler/api_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Event handler for Amazon API Gateway REST/HTTP APIs and Application Loader Balan
* Integrates with [Data classes utilities](../../utilities/data_classes.md){target="_blank"} to easily access event and identity information
* Built-in support for Decimals JSON encoding
* Support for dynamic path expressions
* Router to allow for splitting up the handler accross multiple files

## Getting started

Expand Down Expand Up @@ -75,12 +76,11 @@ This is the sample infrastructure for API Gateway we are using for the examples

Outputs:
HelloWorldApigwURL:
Description: "API Gateway endpoint URL for Prod environment for Hello World Function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello"

HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
Description: "API Gateway endpoint URL for Prod environment for Hello World Function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
```

### API Gateway decorator
Expand Down Expand Up @@ -853,6 +853,264 @@ You can instruct API Gateway handler to use a custom serializer to best suit you
}
```

### Split routes with Router

As you grow the number of routes a given Lambda function should handle, it is natural to split routes into separate files to ease maintenance - That's where the `Router` feature is useful.

Let's assume you have `app.py` as your Lambda function entrypoint and routes in `users.py`, this is how you'd use the `Router` feature.

=== "users.py"

We import **Router** instead of **ApiGatewayResolver**; syntax wise is exactly the same.

```python hl_lines="4 8 12 15 21"
import itertools
from typing import Dict

from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router

logger = Logger(child=True)
router = Router()

USERS = {"user1": "details_here", "user2": "details_here", "user3": "details_here"}

@router.get("/users")
def get_users() -> Dict:
# get query string ?limit=10
pagination_limit = router.current_event.get_query_string_value(name="limit", default_value=10)

logger.info(f"Fetching the first {pagination_limit} users...")
ret = dict(itertools.islice(USERS.items(), pagination_limit))
return {"items": [ret]}

@router.get("/users/<username>")
def get_user(username: str) -> Dict:
logger.info(f"Fetching username {username}")
return {"details": USERS.get(username, {})}

# many other related /users routing
```

=== "app.py"

We use `include_router` method and include all user routers registered in the `router` global object.

```python hl_lines="6 8-9"
from typing import Dict

from aws_lambda_powertools.event_handler import ApiGatewayResolver
from aws_lambda_powertools.utilities.typing import LambdaContext

import users

app = ApiGatewayResolver()
app.include_router(users.router)


def lambda_handler(event: Dict, context: LambdaContext):
app.resolve(event, context)
```

#### Route prefix

As routes are now split in their own files, you can optionally instruct `Router` to inject a prefix for all routes during registration.

In the previous example, `users.py` routes had a `/users` prefix. We could remove `/users` from all route definitions, and then set `include_router(users.router, prefix="/users")` in the `app.py`.

=== "app.py"

```python hl_lines="9"
from typing import Dict

from aws_lambda_powertools.event_handler import ApiGatewayResolver
from aws_lambda_powertools.utilities.typing import LambdaContext

import users

app = ApiGatewayResolver()
app.include_router(users.router, prefix="/users") # prefix '/users' to any route in `users.router`

def lambda_handler(event: Dict, context: LambdaContext):
app.resolve(event, context)
```

=== "users.py"

```python hl_lines="11 15"
from typing import Dict

from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router

logger = Logger(child=True)
router = Router()

USERS = {"user1": "details_here", "user2": "details_here", "user3": "details_here"}

@router.get("/") # /users, when we set the prefix in app.py
def get_users() -> Dict:
...

@router.get("/<username>")
def get_user(username: str) -> Dict:
...

# many other related /users routing
```

#### Sample larger layout

Below is an example project layout where we have Users routes similar to the previous example, and health check route - We use ALB to demonstrate the UX remains the same.

Note that this layout optimizes for code sharing and for those familiar with Python modules. This means multiple functions will share the same `CodeUri` and package, though they are only built once.

!!! tip "External dependencies can be [built as a Lambda Layer](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/building-layers.html){target="_blank"} and set as `dev` dependencies for the project, though outside of scope for this documentation."

=== "Project layout"

```python hl_lines="10-13"
.
├── Pipfile # project dev dependencies
├── Pipfile.lock
├── src
│ ├── __init__.py
│ ├── requirements.txt # dummy for `sam build`, as external deps are Lambda Layers
│ └── app
│ ├── __init__.py # this file makes "app" a "Python package"
│ ├── main.py # Main lambda handler (app.py, index.py, handler.py)
│ └── routers # routers module
│ ├── __init__.py # this file makes "routers" a "Python package"
│ ├── health.py # "health" submodule, e.g. from .routers import health
│ └── users.py # "users" submodule, e.g. from .routers import users
├── template.yaml # SAM template.yml
└── tests
├── __init__.py
├── unit
│ ├── __init__.py
│ └── test_health.py # unit tests for the health router
└── functional
├── __init__.py
├── conftest.py # pytest fixtures for the functional tests
└── test_app_main.py # functional tests for the main lambda handler
```

=== "template.yml"

```yaml hl_lines="22 23"
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Example service with multiple routes

Globals:
Function:
Timeout: 20
MemorySize: 512
Runtime: python3.9
Tracing: Active
Environment:
Variables:
LOG_LEVEL: INFO
POWERTOOLS_LOGGER_LOG_EVENT: true
POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication
POWERTOOLS_SERVICE_NAME: ServiceName

Resources:
AppFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.main.lambda_handler
CodeUri: src
Description: App function description
Events:
HealthPath:
Type: Api
Properties:
Path: /health/status
Method: GET
UserPath:
Type: Api
Properties:
Path: /users/{name}
Method: GET
Environment:
Variables:
PARAM1: VALUE
Tags:
LambdaPowertools: python
Outputs:
AppApigwURL:
Description: "API Gateway endpoint URL for Prod environment for App Function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/app"
AppFunction:
Description: "App Lambda Function ARN"
Value: !GetAtt AppFunction.Arn
```

=== "src/app/main.py"

```python hl_lines="9 14-16"
from typing import Dict

from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler import ApiGatewayResolver
from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType
from aws_lambda_powertools.logging.correlation_paths import API_GATEWAY_HTTP
from aws_lambda_powertools.utilities.typing import LambdaContext

from .routers import health, users

tracer = Tracer()
logger = Logger()
app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent)
app.include_router(health.router, prefix="/health")
app.include_router(users.router)


@logger.inject_lambda_context(correlation_id_path=API_GATEWAY_HTTP)
@tracer.capture_lambda_handler
def lambda_handler(event: Dict, context: LambdaContext):
app.resolve(event, context)
```

=== "src/app/routers/health.py"

```python hl_lines="4 6-7 10 12"
from typing import Dict

from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router

logger = Logger(child=True)
router = Router()


@router.get("/status")
def health() -> Dict:
logger.debug("Health check called")
return {"status": "OK"}
```

=== "tests/functional/test_app_main.py"

```python hl_lines="3"
import json

from src.app import main


def test_lambda_handler(apigw_event, lambda_context):
ret = main.lambda_handler(apigw_event, lambda_context)
expected = json.dumps({"message": "hello universe"}, separators=(",", ":"))

assert ret["statusCode"] == 200
assert ret["body"] == expected
```

#### Trade-offs

!!! todo "TODO"

## Testing your code

You can test your routes by passing a proxy event request where `path` and `httpMethod`.
Expand Down