From 1ea71442a48d5e555dde746adf36dc644a4c1433 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Thu, 20 Nov 2025 11:38:46 +0000 Subject: [PATCH 1/4] Added documentation for Split ROutes --- docs/features/event-handler/rest.md | 38 ++++++++++++++++--- .../event-handler/rest/split_route.ts | 7 ++++ .../event-handler/rest/split_route_index.ts | 11 ++++++ .../event-handler/rest/split_route_prefix.ts | 7 ++++ .../rest/split_route_prefix_index.ts | 11 ++++++ 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 examples/snippets/event-handler/rest/split_route.ts create mode 100644 examples/snippets/event-handler/rest/split_route_index.ts create mode 100644 examples/snippets/event-handler/rest/split_route_prefix.ts create mode 100644 examples/snippets/event-handler/rest/split_route_prefix_index.ts diff --git a/docs/features/event-handler/rest.md b/docs/features/event-handler/rest.md index 5edcc7692f..50d67b92e7 100644 --- a/docs/features/event-handler/rest.md +++ b/docs/features/event-handler/rest.md @@ -565,15 +565,43 @@ Please [check this issue](https://github.com/aws-powertools/powertools-lambda-ty ### Split routers -!!! note "Coming soon" - As applications grow and the number of routes a Lambda function handles increases, it becomes natural to either break it into smaller Lambda functions or split routes into separate files to ease maintenance. -Currently, the TypeScript event-handler's Router class doesn't provide a way to compose multiple router instances, forcing developers to define all routes in a single file or manually merge route definitions. +The `Router` class provide an `includeRouter` method to compose multiple router instances allowing developers to define routes in multiple files and merge route definitions. You will be able to define routes in separate files and import them into a main router file, improving code organization and maintainability. + +!!! note "Merging with Global Middleware" + When merging two `Router` instances together, if you have a global middleware defined in one of your instances, the global middleware gets applied to the all the merged routes. + +Let's assume you have `index.ts` as your Lambda function entrypoint and routes in `split_route.ts`. This is how you'd use the `includeRouter` feature. + +=== "split_route.ts" + + ```typescript + --8<-- "examples/snippets/event-handler/rest/split_route.ts" + ``` + +=== "index.ts" + + ```typescript hl_lines="8" + --8<-- "examples/snippets/event-handler/rest/split_route_index.ts" + ``` + +#### Route Prefix + +In the previous example, `split_route.ts` routes had a `/todos` prefix. This might grow over time and become repetitive. + +When necessary, you can set a prefix when including a `Router` instance. This means you could remove `/todos` prefix altogether. -Once this feature is available, you will be able to define routes in separate files and import them into a main router file, improving code organization and maintainability. +=== "split_route_prefix.ts" -Please [check this issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/4481) for more details and examples, and add 👍 if you would like us to prioritize it. + ```typescript + --8<-- "examples/snippets/event-handler/rest/split_route_prefix.ts" + ``` +=== "index.ts" + + ```typescript hl_lines="8" + --8<-- "examples/snippets/event-handler/rest/split_route_prefix_index.ts" + ``` ### Considerations diff --git a/examples/snippets/event-handler/rest/split_route.ts b/examples/snippets/event-handler/rest/split_route.ts new file mode 100644 index 0000000000..d6ea8bcc41 --- /dev/null +++ b/examples/snippets/event-handler/rest/split_route.ts @@ -0,0 +1,7 @@ +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; + +const router = new Router(); +router.get('/todos', () => 'Get all todos'); +router.get('/todos/:id', () => 'Get a single todo item'); + +export { router }; diff --git a/examples/snippets/event-handler/rest/split_route_index.ts b/examples/snippets/event-handler/rest/split_route_index.ts new file mode 100644 index 0000000000..5ba49cf24b --- /dev/null +++ b/examples/snippets/event-handler/rest/split_route_index.ts @@ -0,0 +1,11 @@ +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import type { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import { router } from './split_route'; + +const app = new Router(); + +// Split Routers +app.includeRouter(router); + +export const handler = async (event: APIGatewayProxyEvent, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/split_route_prefix.ts b/examples/snippets/event-handler/rest/split_route_prefix.ts new file mode 100644 index 0000000000..1a51ae4530 --- /dev/null +++ b/examples/snippets/event-handler/rest/split_route_prefix.ts @@ -0,0 +1,7 @@ +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; + +const router = new Router(); +router.get('/', () => 'Get all todos'); +router.get('/:id', () => 'Get a single todo item'); + +export { router }; diff --git a/examples/snippets/event-handler/rest/split_route_prefix_index.ts b/examples/snippets/event-handler/rest/split_route_prefix_index.ts new file mode 100644 index 0000000000..d42f8d04f8 --- /dev/null +++ b/examples/snippets/event-handler/rest/split_route_prefix_index.ts @@ -0,0 +1,11 @@ +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import type { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import { router } from './split_route'; + +const app = new Router(); + +// Split Routers +app.includeRouter(router, { prefix: '/todos' }); + +export const handler = async (event: APIGatewayProxyEvent, context: Context) => + app.resolve(event, context); From 5975cc8f4c8d299c874c312457fbbc22dcbee894 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Thu, 20 Nov 2025 13:53:26 +0000 Subject: [PATCH 2/4] Added documentation for built-in error handlers and fixed some docs for CORS --- docs/features/event-handler/rest.md | 29 ++++++++++--------- ... gettingStarted_built_in_error_handler.ts} | 8 +++++ 2 files changed, 23 insertions(+), 14 deletions(-) rename examples/snippets/event-handler/rest/{gettingStarted_handle_not_found.ts => gettingStarted_built_in_error_handler.ts} (81%) diff --git a/docs/features/event-handler/rest.md b/docs/features/event-handler/rest.md index 50d67b92e7..92e31f2eb3 100644 --- a/docs/features/event-handler/rest.md +++ b/docs/features/event-handler/rest.md @@ -130,18 +130,6 @@ Please [check this issue](https://github.com/aws-powertools/powertools-lambda-ty You can access request details such as headers, query parameters, and body using the `Request` object provided to your route handlers and middleware functions via `reqCtx.req`. -### Handling not found routes - -By default, we return a `404 Not Found` response for any unmatched routes. - -You can use the `notFound()` method as a higher-order function or class method decorator to override this behavior, and return a custom response. - -=== "index.ts" - - ```ts hl_lines="11" - --8<-- "examples/snippets/event-handler/rest/gettingStarted_handle_not_found.ts" - ``` - ### Error handling You can use the `errorHandler()` method as a higher-order function or class method decorator to define a custom error handler for errors thrown in your route handlers or middleware. @@ -158,6 +146,20 @@ Error handlers receive the error object and the request context as arguments, an --8<-- "examples/snippets/event-handler/rest/gettingStarted_error_handling.ts:4" ``` +### Built-in Error Handlers + +We provide built-in error handlers for common routing errors so you don't have to specify the Error type explicitly. + +You can use the `notFound()` and `methodNotAllowed()` methods as higher-order functions or class method decorators to customize error responses for unmatched routes and unsupported HTTP methods. + +By default, we return a `404 Not Found` response for unmatched routes. + +=== "index.ts" + + ```ts hl_lines="11 23" + --8<-- "examples/snippets/event-handler/rest/gettingStarted_built_in_error_handler.ts" + ``` + ### Throwing HTTP errors You can throw HTTP errors in your route handlers to stop execution and return specific HTTP status codes and messages. Event Handler provides a set of built-in HTTP error classes that you can use to throw common HTTP errors. @@ -439,11 +441,10 @@ For convenience, these are the default CORS settings applied when you register t | Key | Default Value | Description | | --------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `origin` | `*` | Specifies the allowed origin(s) that can access the resource. Use `*` to allow all origins. | -| `methods` | `GET,HEAD,PUT,PATCH,POST,DELETE` | Specifies the allowed HTTP methods. | +| `methods` | `['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']` | Specifies the allowed HTTP methods. | | `allowHeaders` | `[Authorization, Content-Type, X-Amz-Date, X-Api-Key, X-Amz-Security-Token]` | Specifies the allowed headers that can be used in the actual request. | | `exposeHeaders` | `[]` | Any additional header beyond the [safe listed by CORS specification](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header){target="_blank"}. | | `credentials` | `false` | Only necessary when you need to expose cookies, authorization headers or TLS client certificates. | -| `maxAge` | `0` | Indicates how long the results of a preflight request can be cached. Value is in seconds. | #### Per-route overrides diff --git a/examples/snippets/event-handler/rest/gettingStarted_handle_not_found.ts b/examples/snippets/event-handler/rest/gettingStarted_built_in_error_handler.ts similarity index 81% rename from examples/snippets/event-handler/rest/gettingStarted_handle_not_found.ts rename to examples/snippets/event-handler/rest/gettingStarted_built_in_error_handler.ts index fe7e402b8b..8f59af2398 100644 --- a/examples/snippets/event-handler/rest/gettingStarted_handle_not_found.ts +++ b/examples/snippets/event-handler/rest/gettingStarted_built_in_error_handler.ts @@ -20,5 +20,13 @@ app.notFound(async (error, reqCtx) => { }; }); +app.methodNotAllowed(async (error) => { + logger.error('Method not allowed', { error }); + + return { + body: 'This method is not allowed', + }; +}); + export const handler = async (event: unknown, context: Context) => app.resolve(event, context); From 2a63628f4feb82ce974fcf8c5bec32ba8c60d358 Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Thu, 20 Nov 2025 14:45:14 +0000 Subject: [PATCH 3/4] Added documentation for catch all route --- docs/features/event-handler/rest.md | 29 +++++++++++++++++++ ...gettingStarted_dynamic_routes_catch_all.ts | 23 +++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 examples/snippets/event-handler/rest/gettingStarted_dynamic_routes_catch_all.ts diff --git a/docs/features/event-handler/rest.md b/docs/features/event-handler/rest.md index 92e31f2eb3..738e026f4c 100644 --- a/docs/features/event-handler/rest.md +++ b/docs/features/event-handler/rest.md @@ -91,6 +91,35 @@ All dynamic route parameters will be available as typed object properties in the You can also nest dynamic paths, for example `/todos/:todoId/comments/:commentId`, where both `:todoId` and `:commentId` will be resolved at runtime. +#### Catch-all routes + +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. + +**We recommend** having explicit routes whenever possible; use catch-all routes sparingly. + +##### Using Regex Patterns + +You can use standard [Regular Expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions){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` | + +=== "gettingStarted_dynamic_routes_catch_all.ts" + + ```python hl_lines="7 10 13 20" + --8<-- "examples/snippets/event-handler/rest/gettingStarted_dynamic_routes_catch_all.ts" + ``` + +???+ 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 You can use dedicated methods to specify the HTTP method that should be handled in each resolver. That is, `app.()`, where the HTTP method could be `delete`, `get`, `head`, `patch`, `post`, `put`, `options`. diff --git a/examples/snippets/event-handler/rest/gettingStarted_dynamic_routes_catch_all.ts b/examples/snippets/event-handler/rest/gettingStarted_dynamic_routes_catch_all.ts new file mode 100644 index 0000000000..388b0e0f38 --- /dev/null +++ b/examples/snippets/event-handler/rest/gettingStarted_dynamic_routes_catch_all.ts @@ -0,0 +1,23 @@ +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import type { APIGatewayProxyEvent, Context } from 'aws-lambda'; + +const app = new Router(); + +// File path proxy +app.get(/\/files\/.+/, () => 'Catch any GET method under /files'); + +// API versioning with any format +app.get(/\/api\/v\d+\/.*/, () => 'Catch any GET method under /api/vX'); + +// Mixed: dynamic parameter + regex catch-all +app.get(/\/users\/:userId\/files\/.+/, (reqCtx) => { + return { + userId: reqCtx.params.userId, + }; +}); + +// Catch all route +app.get(/.+/, () => 'Catch any GET method'); + +export const handler = async (event: APIGatewayProxyEvent, context: Context) => + app.resolve(event, context); From 53185047ac098ef0123409947c19a13ab94b343c Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Thu, 20 Nov 2025 15:18:46 +0000 Subject: [PATCH 4/4] Addressed feedbacks --- docs/features/event-handler/rest.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/features/event-handler/rest.md b/docs/features/event-handler/rest.md index 738e026f4c..b2726f88cf 100644 --- a/docs/features/event-handler/rest.md +++ b/docs/features/event-handler/rest.md @@ -99,7 +99,7 @@ For scenarios where you need to handle arbitrary or deeply nested paths, you can ##### Using Regex Patterns -You can use standard [Regular Expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions){target="_blank" rel="nofollow"} in your route definitions, for example: +You can use standard [regular expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions){target="_blank" rel="nofollow"} in your route definitions, for example: | Pattern | Description | Examples | |-----------|------------------------------------------|-------------------------------------------------------------| @@ -115,10 +115,9 @@ You can use standard [Regular Expressions](https://developer.mozilla.org/en-US/d ``` ???+ warning "Route Matching Priority" - - Routes are matched in **order of specificity**, not registration order + - For non-regex routes, 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 + - Among regex routes, registration order determines matching precedence, therefore, always place catch-all routes `/.*/` last ### HTTP Methods @@ -620,7 +619,7 @@ Let's assume you have `index.ts` as your Lambda function entrypoint and routes i In the previous example, `split_route.ts` routes had a `/todos` prefix. This might grow over time and become repetitive. -When necessary, you can set a prefix when including a `Router` instance. This means you could remove `/todos` prefix altogether. +When necessary, you can set a prefix when including a `Router` instance. This means you can remove `/todos` prefix altogether. === "split_route_prefix.ts"