From e822eb5110385aa3bd1b90cd6c13bcacde5d267a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 20 Sep 2025 16:20:59 +0200 Subject: [PATCH 1/2] docs(event-handler): general improvements --- docs/features/event-handler/rest.md | 444 +++++++++++------- .../rest/advanced_binary_responses.ts | 21 + .../event-handler/rest/advanced_compress.ts | 17 + .../rest/advanced_cors_per_route.ts | 26 + .../rest/advanced_cors_simple.ts | 22 + .../rest/advanced_fine_grained_responses.ts | 7 +- .../rest/advanced_mw_compose_middleware.ts | 57 --- .../advanced_mw_compose_middleware_index.ts | 24 + .../advanced_mw_compose_middleware_shared.ts | 23 + .../rest/advanced_mw_custom_middleware.ts | 77 +-- .../rest/advanced_mw_destructuring_problem.ts | 9 +- .../rest/advanced_mw_early_return.ts | 9 +- .../rest/advanced_mw_middleware_order.ts | 6 +- .../rest/advanced_testing_helper.ts | 26 + .../rest/advanced_testing_main.ts | 39 ++ .../diagrams/middleware_early_return.mermaid | 20 + .../middleware_error_handling.mermaid | 19 + .../middleware_execution_order.mermaid | 20 + .../diagrams/middleware_handled_error.mermaid | 21 + .../middleware_throwing_error.mermaid | 19 + .../rest/gettingStarted_error_handling.ts | 30 ++ .../rest/gettingStarted_handle_not_found.ts | 24 + .../gettingStarted_throwing_http_errors.ts | 23 + .../rest/samples/advanced_binary_req.json | 9 + .../rest/samples/advanced_binary_res.json | 13 + .../rest/samples/advanced_compress_req.json | 8 + .../rest/samples/advanced_compress_res.json | 13 + .../rest/samples/advanced_cors_per_route.json | 9 + .../rest/samples/advanced_cors_simple.json | 27 ++ .../rest/samples/advanced_error_debug.json | 9 + .../rest/samples/advanced_error_prod.json | 5 + 31 files changed, 794 insertions(+), 282 deletions(-) create mode 100644 examples/snippets/event-handler/rest/advanced_binary_responses.ts create mode 100644 examples/snippets/event-handler/rest/advanced_compress.ts create mode 100644 examples/snippets/event-handler/rest/advanced_cors_per_route.ts create mode 100644 examples/snippets/event-handler/rest/advanced_cors_simple.ts delete mode 100644 examples/snippets/event-handler/rest/advanced_mw_compose_middleware.ts create mode 100644 examples/snippets/event-handler/rest/advanced_mw_compose_middleware_index.ts create mode 100644 examples/snippets/event-handler/rest/advanced_mw_compose_middleware_shared.ts create mode 100644 examples/snippets/event-handler/rest/advanced_testing_helper.ts create mode 100644 examples/snippets/event-handler/rest/advanced_testing_main.ts create mode 100644 examples/snippets/event-handler/rest/diagrams/middleware_early_return.mermaid create mode 100644 examples/snippets/event-handler/rest/diagrams/middleware_error_handling.mermaid create mode 100644 examples/snippets/event-handler/rest/diagrams/middleware_execution_order.mermaid create mode 100644 examples/snippets/event-handler/rest/diagrams/middleware_handled_error.mermaid create mode 100644 examples/snippets/event-handler/rest/diagrams/middleware_throwing_error.mermaid create mode 100644 examples/snippets/event-handler/rest/gettingStarted_error_handling.ts create mode 100644 examples/snippets/event-handler/rest/gettingStarted_handle_not_found.ts create mode 100644 examples/snippets/event-handler/rest/gettingStarted_throwing_http_errors.ts create mode 100644 examples/snippets/event-handler/rest/samples/advanced_binary_req.json create mode 100644 examples/snippets/event-handler/rest/samples/advanced_binary_res.json create mode 100644 examples/snippets/event-handler/rest/samples/advanced_compress_req.json create mode 100644 examples/snippets/event-handler/rest/samples/advanced_compress_res.json create mode 100644 examples/snippets/event-handler/rest/samples/advanced_cors_per_route.json create mode 100644 examples/snippets/event-handler/rest/samples/advanced_cors_simple.json create mode 100644 examples/snippets/event-handler/rest/samples/advanced_error_debug.json create mode 100644 examples/snippets/event-handler/rest/samples/advanced_error_prod.json diff --git a/docs/features/event-handler/rest.md b/docs/features/event-handler/rest.md index 8de6d6d108..349739bdc8 100644 --- a/docs/features/event-handler/rest.md +++ b/docs/features/event-handler/rest.md @@ -1,25 +1,25 @@ --- -title: REST API -description: Event handler for building REST APIs in AWS Lambda +title: HTTP API +description: Event handler for building HTTP APIs in AWS Lambda status: new --- !!! warning "Feature status" This feature is under active development and may undergo significant changes. We recommend using it in non-critical workloads and [providing feedback](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose){target="_blank"} to help us improve it. -Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), Lambda Function URLs, and VPC Lattice. +Event handler for Amazon API Gateway REST . ## Key Features -* Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API, ALB and Lambda Function URLs. -* Built-in middleware engine for request/response transformation and validation. -* Works with micro function (one or a few routes) and monolithic functions (all routes) +* Lightweight routing to reduce boilerplate for API Gateway REST (HTTP API, ALB and Lambda Function URLs coming soon) +* Built-in middleware engine for request/response transformation (validation coming soon). +* Works with micro function (one or a few routes) and monolithic functions (see [Considerations](#considerations)). ## Getting started ### Install -!!! info "This is not necessary if you're installing Powertools for AWS Lambda (TypeScript) via [Lambda layer](../../getting-started/lambda-layers.md)." + ```shell npm install @aws-lambda-powertools/event-handler @@ -27,11 +27,11 @@ npm install @aws-lambda-powertools/event-handler ### Required resources -If you're using any API Gateway integration, you must have an existing [API Gateway Proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html){target="_blank"} or [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html){target="_blank"} configured to invoke your Lambda function. +If you're using any API Gateway integration, you must have an existing [API Gateway Proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html){target="_blank"} configured to invoke your Lambda function. -In case of using [VPC Lattice](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html){target="_blank"}, you must have a service network configured to invoke your Lambda function. + -This is the sample infrastructure for API Gateway and Lambda Function URLs we are using for the examples in this documentation. There is no additional permissions or dependencies required to use this utility. +This is the sample infrastructure for API Gateway we are using for the examples in this documentation. There is no additional permissions or dependencies required to use this utility. ??? "See Infrastructure as Code (IaC) examples" === "API Gateway SAM Template" @@ -48,11 +48,11 @@ When a request is received, the event handler will automatically convert the eve #### Response auto-serialization -!!! tip "Want full control over the response, headers, and status code? Read about it in the [Fine grained responses](#fine-grained-responses) section." +!!! tip "Want full control over the response, headers, and status code? Read how to [return `Response` objects](#returning-response-objects) directly." For your convenience, when you return a JavaScript object from your route handler, we automatically perform these actions: -* Auto-serialize the response to JSON and trim whitespace +* Auto-serialize the response to JSON * Include the response under the appropriate equivalent of a `body` * Set the `Content-Type` header to `application/json` * Set the HTTP status code to 200 (OK) @@ -63,7 +63,7 @@ For your convenience, when you return a JavaScript object from your route handle --8<-- "examples/snippets/event-handler/rest/gettingStarted_serialization.ts" ``` - 1. This object will be serialized, trimmed, and included under the `body` key + 1. This object will be serialized and included under the `body` key === "JSON response" @@ -93,7 +93,7 @@ You can also nest dynamic paths, for example `/todos/:todoId/comments/:commentId ### 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`. +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`. === "index.ts" @@ -107,7 +107,7 @@ You can use dedicated methods to specify the HTTP method that should be handled --8<-- "examples/snippets/event-handler/rest/samples/gettingStarted_methods.json" ``` -If you need to accept multiple HTTP methods in a single function, or support an HTTP method for which no dedicated method exists (i.e. [`TRACE`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/TRACE){target="_blank"}), you can use the `route` method and pass a list of HTTP methods. +If you need to accept multiple HTTP methods in a single function, or support an HTTP method for which no dedicated method exists (i.e. [`TRACE`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/TRACE){target="_blank"}), you can use the `route()` method and pass a list of HTTP methods. === "index.ts" @@ -116,50 +116,96 @@ If you need to accept multiple HTTP methods in a single function, or support an ``` !!! tip - We generally recommend to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used. + We recommend defining separate route handlers for each HTTP method within your Lambda function, as the functionality typically differs between operations such as `GET`, `POST`, `PUT`, `DELETE` etc ### Data validation !!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose) if you would like us to prioritize this feature. + +We plan to add built-in support for request and response validation using [Standard Schema](https://standardschema.dev){target="_blank"} in a future release. For the time being, you can use any validation library of your choice in your route handlers or middleware. + +Please [check this issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/4516) for more details and examples, and add 👍 if you would like us to prioritize it. ### Accessing request details -You can access request details such as headers, query parameters, and body using the `Request` object provided to your route handlers. +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.request`. ### Handling not found routes -!!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose){target="_blank"} if you would like us to prioritize this feature. +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 -!!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose){target="_blank"} if you would like us to prioritize this feature. +You can use the `errorHandler()` method as higher-order function or class method decorator to define a custom error handler for errors thrown in your route handlers or middleware. + +This allows you to catch and return custom error responses, or perform any other error handling logic you need. + +!!! tip "You can also pass a list of error classes to the `errorHandler()` method." + +=== "index.ts" + + ```ts hl_lines="11" + --8<-- "examples/snippets/event-handler/rest/gettingStarted_error_handling.ts:4" + ``` ### Throwing HTTP errors -!!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose){target="_blank"} if you would like us to prioritize this feature. +You can throw HTTP errors in your route handlers to 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. -### Enabling SwaggerUI +This ensures that your Lambda function doesn't fail but returns a well-defined HTTP error response to the client. -!!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose){target="_blank"} if you would like us to prioritize this feature. +If you need to send custom headers or a different response structure/code, you can use the [Response](#returning-response-objects) object instead. -### Custom domains +=== "index.ts" -!!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose){target="_blank"} if you would like us to prioritize this feature. + ```ts hl_lines="3 10" + --8<-- "examples/snippets/event-handler/rest/gettingStarted_throwing_http_errors.ts:3" + ``` -## Advanced +#### Available HTTP error classes -### CORS +The following HTTP error classes are available for use in your route handlers: + +| Error Class | HTTP Status Code | Description | +|------------------------------|------------------|----------------------------------------------------------------------------------------| +| `BadRequestError` | 400 | Bad Request - The request cannot be fulfilled due to bad syntax | +| `UnauthorizedError` | 401 | Unauthorized - Authentication is required and has failed or not been provided | +| `ForbiddenError` | 403 | Forbidden - The request is valid but the server is refusing action | +| `NotFoundError` | 404 | Not Found - The requested resource could not be found | +| `MethodNotAllowedError` | 405 | Method Not Allowed - The request method is not supported for the requested resource | +| `RequestTimeoutError` | 408 | Request Timeout - The server timed out waiting for the request | +| `RequestEntityTooLargeError` | 413 | Request Entity Too Large - The request is larger than the server is willing to process | +| `InternalServerError` | 500 | Internal Server Error - A generic error message for unexpected server conditions | +| `ServiceUnavailableError` | 503 | Service Unavailable - The server is currently unavailable | + +All error classes accept optional parameters for custom messages and additional details: + +* `message` - Custom error message +* `options` - Standard JavaScript `ErrorOptions` +* `details` - Additional structured data to include in the error response -You can configure CORS at the router level via the `cors` middleware. +### Route prefixes !!! note "Coming soon" +When defining multiple routes related to a specific resource, it's common to have a shared prefix. For example, you might have several routes that all start with `/todos`. + +For example, if you have a custom domain `api.example.com` and you want to map it to the `/v1` base path of your API. In this case, all the requests will contain `/v1/` in the path, requiring you to repeat the `/v1` prefix in all your route definitions. + +At the moment, you have to manually include the prefix in each route definition, however we are planning to add support for route prefixes in a future release. + +Please [check this issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/4513) for more details and examples, and add 👍 if you would like us to prioritize it. + +## Advanced + ### Middleware Middleware are functions that execute during the request-response cycle, sitting between the @@ -169,9 +215,9 @@ cluttering your route handlers. Each middleware function receives the following arguments: -* **params** Route parameters extracted from the URL path -* **reqCtx** Request context containing the event, Lambda context, request, and response objects -* **next** A function to pass control to the next middleware in the chain +* **params** - Route parameters extracted from the URL path +* **reqCtx** - Request context containing the event, Lambda context, request, and response objects +* **next** - A function to pass control to the next middleware in the chain Middleware can be applied on specific routes, globally on all routes, or a combination of both. @@ -181,32 +227,13 @@ for post-processing. When middleware modify the same response properties, the mi executes last in post-processing wins. ```mermaid -sequenceDiagram - participant Request - participant Router - participant GM as Global Middleware - participant RM as Route Middleware - participant Handler as Route Handler - - Request->>Router: Incoming Request - Router->>GM: Execute (params, reqCtx, next) - Note over GM: Pre-processing - GM->>RM: Call next() - Note over RM: Pre-processing - RM->>Handler: Call next() - Note over Handler: Execute handler - Handler-->>RM: Return - Note over RM: Post-processing - RM-->>GM: Return - Note over GM: Post-processing - GM-->>Router: Return - Router-->>Request: Response +--8<-- "examples/snippets/event-handler/rest/diagrams/middleware_execution_order.mermaid" ``` #### Registering middleware -You can use `app.use` to register middleware that should always run regardless of the route +You can use `app.use()` to register middleware that should always run regardless of the route and you can apply middleware to specific routes by passing them as arguments before the route handler. @@ -216,7 +243,7 @@ handler. --8<-- "examples/snippets/event-handler/rest/advanced_mw_middleware_order.ts:3" ``` -=== "JSON Response" +=== "Response" ```json hl_lines="6-7" --8<-- "examples/snippets/event-handler/rest/samples/advanced_mw_middleware_order.json" @@ -226,31 +253,13 @@ handler. There are cases where you may want to terminate the execution of the middleware chain early. To do so, middleware can short-circuit processing by returning a `Response` or JSON object -instead of calling `next()`. Neither the handler nor any subsequent middleware will run +instead of calling `await next()`. + +Neither the handler nor any subsequent middleware will run but the post-processing of already executed middleware will. ```mermaid -sequenceDiagram - participant Request - participant Router - participant M1 as Middleware 1 - participant M2 as Middleware 2 - participant M3 as Middleware 3 - participant Handler as Route Handler - - Request->>Router: Incoming Request - Router->>M1: Execute (params, reqCtx, next) - Note over M1: Pre-processing - M1->>M2: Call next() - Note over M2: Pre-processing - M2->>M2: Return Response (early return) - Note over M2: Post-processing - M2-->>M1: Return Response - Note over M1: Post-processing - M1-->>Router: Return Response - Router-->>Request: Response - Note over M3,Handler: Never executed - +--8<-- "examples/snippets/event-handler/rest/diagrams/middleware_early_return.mermaid" ``` === "index.ts" @@ -259,7 +268,7 @@ sequenceDiagram --8<-- "examples/snippets/event-handler/rest/advanced_mw_early_return.ts:3" ``` -=== "JSON Response" +=== "Response" ```json hl_lines="2" --8<-- "examples/snippets/event-handler/rest/samples/advanced_mw_early_return.json" @@ -272,26 +281,7 @@ By default, any unhandled error in the middleware chain will be propagated as a chain entirely and no post-processing steps for any previously executed middleware will occur. ```mermaid -sequenceDiagram - participant Request - participant Router - participant EH as Error Handler - participant M1 as Middleware 1 - participant M2 as Middleware 2 - participant Handler as Route Handler - - Request->>Router: Incoming Request - Router->>M1: Execute (params, reqCtx, next) - Note over M1: Pre-processing - M1->>M2: Call next() - Note over M2: Throws Error - M2-->>M1: Error propagated - M1-->>Router: Error propagated - Router->>EH: Handle error - EH-->>Router: HTTP 500 Response - Router-->>Request: HTTP 500 Error - Note over Handler: Never executed - +--8<-- "examples/snippets/event-handler/rest/diagrams/middleware_error_handling.mermaid" ```
*Unhandled errors*
@@ -300,28 +290,7 @@ You can handle errors in middleware as you would anywhere else, simply surround a `try`/`catch` block and processing will occur as usual. ```mermaid -sequenceDiagram - participant Request - participant Router - participant M1 as Middleware 1 - participant M2 as Middleware 2 - participant Handler as Route Handler - - Request->>Router: Incoming Request - Router->>M1: Execute (params, reqCtx, next) - Note over M1: Pre-processing - M1->>M2: Call next() - Note over M2: Error thrown & caught - Note over M2: Handle error gracefully - M2->>Handler: Call next() - Note over Handler: Execute handler - Handler-->>M2: Return - Note over M2: Post-processing - M2-->>M1: Return - Note over M1: Post-processing - M1-->>Router: Return - Router-->>Request: Response - +--8<-- "examples/snippets/event-handler/rest/diagrams/middleware_handled_error.mermaid" ```
*Handled errors*
@@ -332,25 +301,7 @@ you can use or you can throw a custom error of your own. As noted above, this me that no post-processing of your request will occur. ```mermaid -sequenceDiagram - participant Request - participant Router - participant EH as Error Handler - participant M1 as Middleware 1 - participant M2 as Middleware 2 - participant Handler as Route Handler - - Request->>Router: Incoming Request - Router->>M1: Execute (params, reqCtx, next) - Note over M1: Pre-processing - M1->>M2: Call next() - Note over M2: Intentionally throws error - M2-->>M1: Error propagated - M1-->>Router: Error propagated - Router->>EH: Handle error - EH-->>Router: HTTP Error Response - Router-->>Request: HTTP Error Response - Note over Handler: Never executed +--8<-- "examples/snippets/event-handler/rest/diagrams/middleware_throwing_error.mermaid" ``` @@ -363,8 +314,8 @@ accepts configuration options and returns a middleware function. === "index.ts" - ```ts hl_lines="5-26 31" - --8<-- "examples/snippets/event-handler/rest/advanced_mw_custom_middleware.ts:3" + ```ts hl_lines="20-21 36 41" + --8<-- "examples/snippets/event-handler/rest/advanced_mw_custom_middleware.ts:8" ``` In this example we have a middleware that acts only in the post-processing stage as all @@ -373,7 +324,7 @@ the handler has run and we have access to request body. #### Avoiding destructuring pitfalls -!!! warning "Critical: Never destructure the response object" +!!! warning "Never destructure the response object" When writing middleware, always access the response through `reqCtx.res` rather than destructuring `{ res }` from the request context. Destructuring captures a reference to the original response object, which becomes stale when middleware replaces the response. === "index.ts" @@ -392,10 +343,16 @@ You can create reusable middleware stacks by using the `composeMiddleware` funct multiple middleware into a single middleware function. This is useful for creating standardized middleware combinations that can be shared across different routes or applications. +=== "middleware.ts" + + ```ts hl_lines="1 21" + --8<-- "examples/snippets/event-handler/rest/advanced_mw_compose_middleware_shared.ts" + ``` + === "index.ts" - ```ts hl_lines="33-34 39" - --8<-- "examples/snippets/event-handler/rest/advanced_mw_compose_middleware.ts:3" + ```ts hl_lines="3 7" + --8<-- "examples/snippets/event-handler/rest/advanced_mw_compose_middleware_index.ts:3" ``` The `composeMiddleware` function maintains the same execution order as if you had applied the @@ -417,74 +374,192 @@ Keep the following in mind when authoring middleware for Event Handler: * **Catch your own errors.** Catch and handle known errors to your logic, unless you want to raise HTTP Errors, or propagate specific errors to the client. * **Avoid destructuring the response object.** As mentioned in the [destructuring pitfalls](#avoiding-destructuring-pitfalls) section, always access the response through `reqCtx.res` rather than destructuring to avoid stale references. -### Fine grained responses +### Returning `Response` objects You can use the Web API's `Response` object to have full control over the response. For example, you might want to add additional headers, cookies, or set a custom content type. === "index.ts" - ```ts hl_lines="11-19 25-32" + ```ts hl_lines="11-18 25-31" --8<-- "examples/snippets/event-handler/rest/advanced_fine_grained_responses.ts:6" ``` -=== "JSON Response" +=== "Response" ```json hl_lines="4-6" --8<-- "examples/snippets/event-handler/rest/samples/advanced_fine_grained_responses.json" ``` -### Response streaming +### CORS -!!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose) if you would like us to prioritize this feature. +You can configure CORS (Cross-Origin Resource Sharing) by using the `cors` middleware. + +This will ensure that CORS headers are returned as part of the response when your functions match the path invoked and the Origin matches one of the allowed values. + +=== "index.ts" + + ```ts hl_lines="2 8-11" + --8<-- "examples/snippets/event-handler/rest/advanced_cors_simple.ts:3" + ``` + +=== "Response" + + ```json + --8<-- "examples/snippets/event-handler/rest/samples/advanced_cors_simple.json" + ``` + +#### Pre-flight + +Pre-flight (`OPTIONS`) requests are typically handled at the API Gateway or Lambda Function URL as per our [sample infrastructure](#required-resources), no Lambda integration is necessary. However, ALB expects you to handle pre-flight requests in your function. + +For convenience, when you register the `cors` middleware, we automatically handle these requests for you as long as the path matches and the `Origin` header is present and valid. + +#### Defaults + +For convenience, these are the default CORS settings applied when you register the `cors` middleware without any options: + +!!! warning "Security consideration" + Always set the `origin` option to a specific domain or list of domains in production environments to avoid security risks associated with allowing all origins. + +| 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. | +| `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 + +You can override the global CORS settings on a per-route basis by passing options to the `cors` middleware when applying it to a specific route. + +=== "index.ts" + + ```ts hl_lines="2 19" + --8<-- "examples/snippets/event-handler/rest/advanced_cors_per_route.ts:3" + ``` + +=== "Response" + + ```json + --8<-- "examples/snippets/event-handler/rest/samples/advanced_cors_per_route.json" + ``` ### Compress -You can compress with gzip and base64 encode your responses via the `compress` parameter. You have the option to pass the `compress` parameter when working with a specific route or setting the correct `Accept-Encoding` header in the `Response` object. +You can enable response compression by using the `compress` middleware. This will automatically compress responses using gzip and base64 encode them when the client indicates support via the `Accept-Encoding` header. -!!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose) if you would like us to prioritize this feature. +=== "index.ts" + + ```ts hl_lines="2 7" + --8<-- "examples/snippets/event-handler/rest/advanced_compress.ts:3" + ``` + +=== "Request" + + ```json hl_lines="3" + --8<-- "examples/snippets/event-handler/rest/samples/advanced_compress_req.json" + ``` + +=== "Response" + + ```json hl_lines="7-9 11 12" + --8<-- "examples/snippets/event-handler/rest/samples/advanced_compress_res.json" + ``` ### Binary responses -!!! warning "Using API Gateway?" - Amazon API Gateway does not support `*/*` binary media type when [CORS](#cors) is also configured. This feature requires API Gateway to configure binary media types, see our [sample infrastructure](#required-resources) for reference. +!!! note "Coming soon" + +As described in the [response auto serialization](#response-auto-serialization) section, when you return a JavaScript object from your route handler, we automatically serialize it to JSON and set the `Content-Type` header to `application/json`. -For convenience, we automatically base64 encode binary responses. You can also use it in combination with the `compress` parameter if your client supports gzip. +If you need to return binary data (e.g. images, PDFs, etc), you will need to return an API Gateway Proxy result directly, setting the `isBase64Encoded` flag to `true` and base64 encoding the binary data, as well as setting the appropriate `Content-Type` header. -Like the `compress` feature, the client must send the `Accept` header with the correct media type. +=== "index.ts" -!!! tip - Lambda Function URLs handle binary media types automatically. + ```ts hl_lines="10-17" + --8<-- "examples/snippets/event-handler/rest/advanced_binary_responses.ts" + ``` + +=== "Request" + + ```json + --8<-- "examples/snippets/event-handler/rest/samples/advanced_binary_req.json" + ``` + +=== "Response" + + ```json + --8<-- "examples/snippets/event-handler/rest/samples/advanced_binary_res.json" + ``` + +You can use binary responses together with the [`compress`](#compress) feature, and the client must send the `Accept` header with the correct media type. + +We plan to add first-class support for binary responses in a future release. Please [check this issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/4514) for more details and examples, and add 👍 if you would like us to prioritize it. + +### Response streaming !!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose) if you would like us to prioritize this feature. + +At the moment, Event Handler does not support streaming responses. This means that the entire response must be generated and returned by the route handler before it can be sent to the client. + +Please [check this issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/4476) for more details and add 👍 if you would like us to prioritize it. ### Debug mode You can enable debug mode via the `POWERTOOLS_DEV` environment variable. -This will enable full stack traces errors in the response, log request and responses, and set CORS in development mode. +When set to `true`, debug mode enhances error responses with detailed information to aid in debugging and testing. + +!!! danger "Security consideration" + Never enable debug mode in production environments as it exposes sensitive error details that could be exploited by attackers. + + Only use it during development and testing. + +#### Enhanced error responses + +When an unhandled error occurs in your route handler or middleware, Event Handler will return a HTTP 500 response by default. + +=== "Production mode (default)" + + ```json + --8<-- "examples/snippets/event-handler/rest/samples/advanced_error_prod.json" + ``` + +=== "Debug mode" + + ```json + --8<-- "examples/snippets/event-handler/rest/samples/advanced_error_debug.json" + ``` + +#### Logging requests and responses !!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose) if you would like us to prioritize this feature. + Please [check this issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/4482) and add 👍 if you would like us to prioritize this feature. ### OpenAPI -When you enable [Data Validation](#data-validation), we use a combination of Zod and JSON Schemas to add constraints to your API's parameters. +!!! note "Coming soon" + +Currently, Event Handler does not support automatic generation of OpenAPI documentation from your route definitions. -In OpenAPI documentation tools like [SwaggerUI](#enabling-swaggerui), these annotations become readable descriptions, offering a self-explanatory API interface. This reduces boilerplate code while improving functionality and enabling auto-documentation. +We plan to add this feature in a future release with an experience similar to what described in the [utility's RFC](https://github.com/aws-powertools/powertools-lambda-typescript/discussions/3500){target="_blank"} and to what available in [Powertools for AWS Lambda (Python)](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#openapi){target="_blank"}. -!!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose) if you would like us to prioritize this feature. +Please [check this issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/4515) for more details, and add 👍 if you would like us to prioritize it. ### Split routers -As you grow the number of routes a given Lambda function should handle, it is natural to either break into smaller Lambda functions, or split routes into separate files to ease maintenance - that's where the split `Router` feature is useful. - !!! note "Coming soon" - Please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose) if you would like us to prioritize this feature. + +As applications grow and the number of routes a Lambda function handles increases, it becomes natural to either break 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. + +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. + +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. ### Considerations @@ -537,5 +612,26 @@ Automated testing, operational and security reviews are essential to stability i ## Testing your code -!!! note "Coming soon" - Please open an issue if you would like us to prioritize this section. +You can use any testing framework of your choice to test Lambda functions using Event Handler. + +Since Event Handler doesn't require any server or socket to run, you can test your code as you would any other JavaScript/TypeScript function. + +Below is an example using [Vitest](https://vitest.dev){target="_blank"}, including a helper function to create mock API Gateway events that you can copy and adapt to your needs. + +=== "index.test.ts" + + ```ts + --8<-- "examples/snippets/event-handler/rest/advanced_testing_main.ts" + ``` + +=== "event_helper.ts" + + ```ts + --8<-- "examples/snippets/event-handler/rest/advanced_testing_helper.ts" + ``` + +=== "index.ts" + + ```ts + --8<-- "examples/snippets/event-handler/rest/advanced_cors_simple.ts:3" + ``` diff --git a/examples/snippets/event-handler/rest/advanced_binary_responses.ts b/examples/snippets/event-handler/rest/advanced_binary_responses.ts new file mode 100644 index 0000000000..a691c3900e --- /dev/null +++ b/examples/snippets/event-handler/rest/advanced_binary_responses.ts @@ -0,0 +1,21 @@ +import { readFile } from 'node:fs/promises'; +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import { compress } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware'; +import type { Context } from 'aws-lambda'; + +const app = new Router(); + +app.get('/logo', [compress()], async () => { + const logoFile = await readFile(`${process.env.LAMBDA_TASK_ROOT}/logo.png`); + return { + body: logoFile.toString('base64'), + isBase64Encoded: true, + headers: { + 'Content-Type': 'image/png', + }, + statusCode: 200, + }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_compress.ts b/examples/snippets/event-handler/rest/advanced_compress.ts new file mode 100644 index 0000000000..44756a1e14 --- /dev/null +++ b/examples/snippets/event-handler/rest/advanced_compress.ts @@ -0,0 +1,17 @@ +declare function getTodoById(todoId: unknown): Promise<{ id: string } & T>; + +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import { compress } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware'; +import type { Context } from 'aws-lambda'; + +const app = new Router(); + +app.use(compress()); + +app.get('/todos/:todoId', async ({ todoId }) => { + const todo = await getTodoById(todoId); + return { todo }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_cors_per_route.ts b/examples/snippets/event-handler/rest/advanced_cors_per_route.ts new file mode 100644 index 0000000000..89a49f942f --- /dev/null +++ b/examples/snippets/event-handler/rest/advanced_cors_per_route.ts @@ -0,0 +1,26 @@ +declare function getTodoById(todoId: unknown): Promise<{ id: string } & T>; + +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import { cors } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware'; +import type { Context } from 'aws-lambda'; + +const app = new Router(); + +app.use( + cors({ + origin: 'https://example.com', + maxAge: 300, + }) +); + +app.get('/todos/:todoId', async ({ todoId }) => { + const todo = await getTodoById(todoId); + return { todo }; +}); + +app.get('/health', [cors({ origin: '*' })], async () => { + return { status: 'ok' }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_cors_simple.ts b/examples/snippets/event-handler/rest/advanced_cors_simple.ts new file mode 100644 index 0000000000..b080185ce1 --- /dev/null +++ b/examples/snippets/event-handler/rest/advanced_cors_simple.ts @@ -0,0 +1,22 @@ +declare function getTodoById(todoId: unknown): Promise<{ id: string } & T>; + +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import { cors } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware'; +import type { Context } from 'aws-lambda'; + +const app = new Router(); + +app.use( + cors({ + origin: 'https://example.com', + maxAge: 300, + }) +); + +app.get('/todos/:todoId', async ({ todoId }) => { + const todo = await getTodoById(todoId); + return { todo }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_fine_grained_responses.ts b/examples/snippets/event-handler/rest/advanced_fine_grained_responses.ts index 2d989f2bdb..26cd5d204c 100644 --- a/examples/snippets/event-handler/rest/advanced_fine_grained_responses.ts +++ b/examples/snippets/event-handler/rest/advanced_fine_grained_responses.ts @@ -23,7 +23,7 @@ app.get('/todos', async () => { }); }); -app.post('/todos', async (params, reqCtx) => { +app.post('/todos', async (_, reqCtx) => { const body = await reqCtx.request.json(); const todo = await createTodo(body.title); @@ -36,6 +36,5 @@ app.post('/todos', async (params, reqCtx) => { }); }); -export const handler = async (event: unknown, context: Context) => { - return app.resolve(event, context); -}; +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_mw_compose_middleware.ts b/examples/snippets/event-handler/rest/advanced_mw_compose_middleware.ts deleted file mode 100644 index 0263371543..0000000000 --- a/examples/snippets/event-handler/rest/advanced_mw_compose_middleware.ts +++ /dev/null @@ -1,57 +0,0 @@ -declare const getAllTodos: () => Promise; -declare const putTodo: (body: any) => Promise; - -import { - composeMiddleware, - Router, -} from '@aws-lambda-powertools/event-handler/experimental-rest'; -import type { Middleware } from '@aws-lambda-powertools/event-handler/types'; -import { Logger } from '@aws-lambda-powertools/logger'; -import type { Context } from 'aws-lambda'; - -const logger = new Logger(); - -// Individual middleware functions -const logging: Middleware = async (params, reqCtx, next) => { - logger.info(`Request: ${reqCtx.request.method} ${reqCtx.request.url}`); - await next(); - logger.info(`Response: ${reqCtx.res.status}`); -}; - -const cors: Middleware = async (params, reqCtx, next) => { - await next(); - reqCtx.res.headers.set('Access-Control-Allow-Origin', '*'); - reqCtx.res.headers.set( - 'Access-Control-Allow-Methods', - 'GET, POST, PUT, DELETE' - ); -}; - -const rateLimit: Middleware = async (params, reqCtx, next) => { - // Rate limiting logic would go here - reqCtx.res.headers.set('X-RateLimit-Limit', '100'); - await next(); -}; - -// Compose middleware stack for all requests -const apiMiddleware = composeMiddleware([logging, cors, rateLimit]); - -const app = new Router(); - -// Use composed middleware globally -app.use(apiMiddleware); - -app.get('/todos', async () => { - const todos = await getAllTodos(); - return { todos }; -}); - -app.post('/todos', async (params, { request }) => { - const body = await request.json(); - const todo = await putTodo(body); - return todo; -}); - -export const handler = async (event: unknown, context: Context) => { - return await app.resolve(event, context); -}; diff --git a/examples/snippets/event-handler/rest/advanced_mw_compose_middleware_index.ts b/examples/snippets/event-handler/rest/advanced_mw_compose_middleware_index.ts new file mode 100644 index 0000000000..e1a26356b3 --- /dev/null +++ b/examples/snippets/event-handler/rest/advanced_mw_compose_middleware_index.ts @@ -0,0 +1,24 @@ +declare const getAllTodos: () => Promise[]>; +declare const putTodo: (body: unknown) => Promise>; + +import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import type { Context } from 'aws-lambda'; +import { apiMiddleware } from './advanced_mw_compose_middleware_shared.js'; + +const app = new Router(); + +app.use(apiMiddleware); + +app.get('/todos', async () => { + const todos = await getAllTodos(); + return { todos }; +}); + +app.post('/todos', async (_, { request }) => { + const body = await request.json(); + const todo = await putTodo(body); + return todo; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_mw_compose_middleware_shared.ts b/examples/snippets/event-handler/rest/advanced_mw_compose_middleware_shared.ts new file mode 100644 index 0000000000..1a475aa13b --- /dev/null +++ b/examples/snippets/event-handler/rest/advanced_mw_compose_middleware_shared.ts @@ -0,0 +1,23 @@ +import { composeMiddleware } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import { cors } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware'; +import type { Middleware } from '@aws-lambda-powertools/event-handler/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const logging: Middleware = async (_, reqCtx, next) => { + logger.info(`Request: ${reqCtx.request.method} ${reqCtx.request.url}`); + await next(); + logger.info(`Response: ${reqCtx.res.status}`); +}; + +const rateLimit: Middleware = async (_, reqCtx, next) => { + // Rate limiting logic would go here + reqCtx.res.headers.set('X-RateLimit-Limit', '100'); + await next(); +}; + +// Reusable composed middleware +const apiMiddleware = composeMiddleware([logging, cors(), rateLimit]); + +export { apiMiddleware }; diff --git a/examples/snippets/event-handler/rest/advanced_mw_custom_middleware.ts b/examples/snippets/event-handler/rest/advanced_mw_custom_middleware.ts index cf9238c48a..05f560c325 100644 --- a/examples/snippets/event-handler/rest/advanced_mw_custom_middleware.ts +++ b/examples/snippets/event-handler/rest/advanced_mw_custom_middleware.ts @@ -1,48 +1,57 @@ -declare const compresssBody: (body: string) => Promise; +declare const getUserTodos: ( + userId: string +) => Promise[]>; +declare const jwt: { + verify(token: string, secret: string): { sub: string; roles: string[] }; +}; -import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest'; +import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env'; +import { + Router, + UnauthorizedError, +} from '@aws-lambda-powertools/event-handler/experimental-rest'; import type { Middleware } from '@aws-lambda-powertools/event-handler/types'; +import { Logger } from '@aws-lambda-powertools/logger'; import type { Context } from 'aws-lambda'; -interface CompressOptions { - threshold?: number; - level?: number; -} - -// Factory function that returns middleware -const compress = (options: CompressOptions = {}): Middleware => { - return async (params, reqCtx, next) => { - await next(); +const jwtSecret = getStringFromEnv({ + key: 'JWT_SECRET', + errorMessage: 'JWT_SECRET is not set', +}); - // Check if response should be compressed - const body = await reqCtx.res.text(); - const threshold = options.threshold || 1024; +const logger = new Logger({}); +const app = new Router(); +const store: { userId: string; roles: string[] } = { userId: '', roles: [] }; - if (body.length > threshold) { - const compressedBody = await compresssBody(body); - const compressedRes = new Response(compressedBody, reqCtx.res); - compressedRes.headers.set('Content-Encoding', 'gzip'); - reqCtx.res = compressedRes; +// Factory function that returns middleware +const verifyToken = (options: { jwtSecret: string }): Middleware => { + return async (_, { request }, next) => { + const auth = request.headers.get('Authorization'); + if (!auth || !auth.startsWith('Bearer ')) + return new UnauthorizedError('Missing or invalid Authorization header'); + + const token = auth.slice(7); + try { + const payload = jwt.verify(token, options.jwtSecret); + store.userId = payload.sub; + store.roles = payload.roles; + } catch (error) { + logger.error('Token verification failed', { error }); + return new UnauthorizedError('Invalid token'); } + + await next(); }; }; -const app = new Router(); - // Use custom middleware globally -app.use(compress({ threshold: 500 })); +app.use(verifyToken({ jwtSecret })); -app.get('/data', async () => { - return { - message: 'Large response data', - data: new Array(100).fill('content'), - }; +app.post('/todos', async (_) => { + const { userId } = store; + const todos = await getUserTodos(userId); + return { todos }; }); -app.get('/small', async () => { - return { message: 'Small response' }; -}); - -export const handler = async (event: unknown, context: Context) => { - return await app.resolve(event, context); -}; +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_mw_destructuring_problem.ts b/examples/snippets/event-handler/rest/advanced_mw_destructuring_problem.ts index cc268fac6b..eaaca2bc2b 100644 --- a/examples/snippets/event-handler/rest/advanced_mw_destructuring_problem.ts +++ b/examples/snippets/event-handler/rest/advanced_mw_destructuring_problem.ts @@ -5,7 +5,7 @@ import type { Context } from 'aws-lambda'; const app = new Router(); // ❌ WRONG: Using destructuring captures a reference to the original response -const badMiddleware: Middleware = async (params, { res }, next) => { +const _badMiddleware: Middleware = async (_, { res }, next) => { res.headers.set('X-Before', 'Before'); await next(); // This header will NOT be added because 'res' is a stale reference @@ -13,7 +13,7 @@ const badMiddleware: Middleware = async (params, { res }, next) => { }; // ✅ CORRECT: Always access response through reqCtx -const goodMiddleware: Middleware = async (params, reqCtx, next) => { +const goodMiddleware: Middleware = async (_, reqCtx, next) => { reqCtx.res.headers.set('X-Before', 'Before'); await next(); // This header WILL be added because we get the current response @@ -26,6 +26,5 @@ app.get('/test', async () => { return { message: 'Hello World!' }; }); -export const handler = async (event: unknown, context: Context) => { - return await app.resolve(event, context); -}; +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_mw_early_return.ts b/examples/snippets/event-handler/rest/advanced_mw_early_return.ts index a08e90e3cf..4a1d26df9e 100644 --- a/examples/snippets/event-handler/rest/advanced_mw_early_return.ts +++ b/examples/snippets/event-handler/rest/advanced_mw_early_return.ts @@ -9,7 +9,7 @@ const logger = new Logger(); const app = new Router({ logger }); // Authentication middleware - returns early if no auth header -const authMiddleware: Middleware = async (params, reqCtx, next) => { +const authMiddleware: Middleware = async (_, reqCtx, next) => { const authHeader = reqCtx.request.headers.get('authorization'); if (!authHeader) { @@ -23,7 +23,7 @@ const authMiddleware: Middleware = async (params, reqCtx, next) => { }; // Logging middleware - never executes when auth fails -const loggingMiddleware: Middleware = async (params, reqCtx, next) => { +const loggingMiddleware: Middleware = async (_, __, next) => { logger.info('Request processed'); await next(); }; @@ -36,6 +36,5 @@ app.get('/todos', async () => { return { todos }; }); -export const handler = async (event: unknown, context: Context) => { - return app.resolve(event, context); -}; +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/advanced_mw_middleware_order.ts b/examples/snippets/event-handler/rest/advanced_mw_middleware_order.ts index dad875582b..fc86013b5e 100644 --- a/examples/snippets/event-handler/rest/advanced_mw_middleware_order.ts +++ b/examples/snippets/event-handler/rest/advanced_mw_middleware_order.ts @@ -10,14 +10,14 @@ const logger = new Logger(); const app = new Router({ logger }); // Global middleware - executes first in pre-processing, last in post-processing -app.use(async (params, reqCtx, next) => { +app.use(async (_, reqCtx, next) => { reqCtx.res.headers.set('x-pre-processed-by', 'global-middleware'); await next(); reqCtx.res.headers.set('x-post-processed-by', 'global-middleware'); }); // Route-specific middleware - executes second in pre-processing, first in post-processing -const routeMiddleware: Middleware = async (params, reqCtx, next) => { +const routeMiddleware: Middleware = async (_, reqCtx, next) => { reqCtx.res.headers.set('x-pre-processed-by', 'route-middleware'); await next(); reqCtx.res.headers.set('x-post-processed-by', 'route-middleware'); @@ -31,7 +31,7 @@ app.get('/todos', async () => { // This route will have: // x-pre-processed-by: route-middleware (route middleware overwrites global) // x-post-processed-by: global-middleware (global middleware executes last) -app.post('/todos', [routeMiddleware], async (params, reqCtx) => { +app.post('/todos', [routeMiddleware], async (_, reqCtx) => { const body = await reqCtx.request.json(); const todo = await putTodo(body); return todo; diff --git a/examples/snippets/event-handler/rest/advanced_testing_helper.ts b/examples/snippets/event-handler/rest/advanced_testing_helper.ts new file mode 100644 index 0000000000..bce078e7c2 --- /dev/null +++ b/examples/snippets/event-handler/rest/advanced_testing_helper.ts @@ -0,0 +1,26 @@ +import type { APIGatewayProxyEvent } from 'aws-lambda'; + +const createTestEvent = (options: { + path: string; + httpMethod: string; + headers?: Record; +}): APIGatewayProxyEvent => ({ + path: options.path, + httpMethod: options.httpMethod, + headers: options.headers ?? {}, + body: null, + multiValueHeaders: {}, + isBase64Encoded: false, + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: { + httpMethod: options.httpMethod, + path: options.path, + domainName: 'localhost', + } as APIGatewayProxyEvent['requestContext'], + resource: '', +}); + +export { createTestEvent }; diff --git a/examples/snippets/event-handler/rest/advanced_testing_main.ts b/examples/snippets/event-handler/rest/advanced_testing_main.ts new file mode 100644 index 0000000000..4fbc32d553 --- /dev/null +++ b/examples/snippets/event-handler/rest/advanced_testing_main.ts @@ -0,0 +1,39 @@ +import type { Context } from 'aws-lambda'; +import { expect, test } from 'vitest'; +import { handler } from './advanced_cors_simple.js'; +import { createTestEvent } from './advanced_testing_helper.js'; + +test('returns CORS headers', async () => { + // Preapare + const event = createTestEvent({ + httpMethod: 'GET', + headers: { + Origin: 'https://example.com', + }, + path: '/todos/123', + }); + + // Act + const result = await handler(event, {} as Context); + + // Assess + expect(result.statusCode).toEqual(200); + expect(result.body).toEqual(JSON.stringify({ todo: { id: '123' } })); + expect(result.headers?.['access-control-allow-origin']).toEqual( + 'https://example.com' + ); + expect( + result.multiValueHeaders?.['access-control-allow-methods'].sort() + ).toEqual(['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'].sort()); + expect( + result.multiValueHeaders?.['access-control-allow-headers'].sort() + ).toEqual( + [ + 'Authorization', + 'Content-Type', + 'X-Amz-Date', + 'X-Amz-Security-Token', + 'X-Api-Key', + ].sort() + ); +}); diff --git a/examples/snippets/event-handler/rest/diagrams/middleware_early_return.mermaid b/examples/snippets/event-handler/rest/diagrams/middleware_early_return.mermaid new file mode 100644 index 0000000000..ca7e189db5 --- /dev/null +++ b/examples/snippets/event-handler/rest/diagrams/middleware_early_return.mermaid @@ -0,0 +1,20 @@ +sequenceDiagram + participant Request + participant Router + participant M1 as Middleware 1 + participant M2 as Middleware 2 + participant M3 as Middleware 3 + participant Handler as Route Handler + + Request->>Router: Incoming Request + Router->>M1: Execute (params, reqCtx, next) + Note over M1: Pre-processing + M1->>M2: Call next() + Note over M2: Pre-processing + M2->>M2: Return Response (early return) + Note over M2: Post-processing + M2-->>M1: Return Response + Note over M1: Post-processing + M1-->>Router: Return Response + Router-->>Request: Response + Note over M3,Handler: Never executed \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/diagrams/middleware_error_handling.mermaid b/examples/snippets/event-handler/rest/diagrams/middleware_error_handling.mermaid new file mode 100644 index 0000000000..5c5aafa569 --- /dev/null +++ b/examples/snippets/event-handler/rest/diagrams/middleware_error_handling.mermaid @@ -0,0 +1,19 @@ +sequenceDiagram + participant Request + participant Router + participant EH as Error Handler + participant M1 as Middleware 1 + participant M2 as Middleware 2 + participant Handler as Route Handler + + Request->>Router: Incoming Request + Router->>M1: Execute (params, reqCtx, next) + Note over M1: Pre-processing + M1->>M2: Call next() + Note over M2: Throws Error + M2-->>M1: Error propagated + M1-->>Router: Error propagated + Router->>EH: Handle error + EH-->>Router: HTTP 500 Response + Router-->>Request: HTTP 500 Error + Note over Handler: Never executed \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/diagrams/middleware_execution_order.mermaid b/examples/snippets/event-handler/rest/diagrams/middleware_execution_order.mermaid new file mode 100644 index 0000000000..696be1264d --- /dev/null +++ b/examples/snippets/event-handler/rest/diagrams/middleware_execution_order.mermaid @@ -0,0 +1,20 @@ +sequenceDiagram + participant Request + participant Router + participant GM as Global Middleware + participant RM as Route Middleware + participant Handler as Route Handler + + Request->>Router: Incoming Request + Router->>GM: Execute (params, reqCtx, next) + Note over GM: Pre-processing + GM->>RM: Call next() + Note over RM: Pre-processing + RM->>Handler: Call next() + Note over Handler: Execute handler + Handler-->>RM: Return + Note over RM: Post-processing + RM-->>GM: Return + Note over GM: Post-processing + GM-->>Router: Return + Router-->>Request: Response \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/diagrams/middleware_handled_error.mermaid b/examples/snippets/event-handler/rest/diagrams/middleware_handled_error.mermaid new file mode 100644 index 0000000000..5d0da29a88 --- /dev/null +++ b/examples/snippets/event-handler/rest/diagrams/middleware_handled_error.mermaid @@ -0,0 +1,21 @@ +sequenceDiagram + participant Request + participant Router + participant M1 as Middleware 1 + participant M2 as Middleware 2 + participant Handler as Route Handler + + Request->>Router: Incoming Request + Router->>M1: Execute (params, reqCtx, next) + Note over M1: Pre-processing + M1->>M2: Call next() + Note over M2: Error thrown & caught + Note over M2: Handle error gracefully + M2->>Handler: Call next() + Note over Handler: Execute handler + Handler-->>M2: Return + Note over M2: Post-processing + M2-->>M1: Return + Note over M1: Post-processing + M1-->>Router: Return + Router-->>Request: Response \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/diagrams/middleware_throwing_error.mermaid b/examples/snippets/event-handler/rest/diagrams/middleware_throwing_error.mermaid new file mode 100644 index 0000000000..25ed59d74d --- /dev/null +++ b/examples/snippets/event-handler/rest/diagrams/middleware_throwing_error.mermaid @@ -0,0 +1,19 @@ +sequenceDiagram + participant Request + participant Router + participant EH as Error Handler + participant M1 as Middleware 1 + participant M2 as Middleware 2 + participant Handler as Route Handler + + Request->>Router: Incoming Request + Router->>M1: Execute (params, reqCtx, next) + Note over M1: Pre-processing + M1->>M2: Call next() + Note over M2: Intentionally throws error + M2-->>M1: Error propagated + M1-->>Router: Error propagated + Router->>EH: Handle error + EH-->>Router: HTTP Error Response + Router-->>Request: HTTP Error Response + Note over Handler: Never executed \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/gettingStarted_error_handling.ts b/examples/snippets/event-handler/rest/gettingStarted_error_handling.ts new file mode 100644 index 0000000000..9465fa6d02 --- /dev/null +++ b/examples/snippets/event-handler/rest/gettingStarted_error_handling.ts @@ -0,0 +1,30 @@ +declare function getTodoById(todoId: unknown): Promise<{ id: string } & T>; +declare class GetTodoError extends Error {} + +import { + HttpErrorCodes, + Router, +} from '@aws-lambda-powertools/event-handler/experimental-rest'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { Context } from 'aws-lambda/handler'; + +const logger = new Logger(); +const app = new Router({ logger }); + +app.errorHandler(GetTodoError, async (error, reqCtx) => { + logger.error('Unable to get todo', { error }); + + return { + statusCode: HttpErrorCodes.BAD_REQUEST, + message: `Bad request: ${error.message} - ${reqCtx.request.headers.get('x-correlation-id')}`, + error: 'BadRequest', + }; +}); + +app.get('/todos/:todoId', async ({ todoId }) => { + const todo = await getTodoById(todoId); // May throw GetTodoError + return { todo }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/gettingStarted_handle_not_found.ts b/examples/snippets/event-handler/rest/gettingStarted_handle_not_found.ts new file mode 100644 index 0000000000..970fc7b294 --- /dev/null +++ b/examples/snippets/event-handler/rest/gettingStarted_handle_not_found.ts @@ -0,0 +1,24 @@ +import { + HttpErrorCodes, + Router, +} from '@aws-lambda-powertools/event-handler/experimental-rest'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { Context } from 'aws-lambda/handler'; + +const logger = new Logger(); +const app = new Router({ logger }); + +app.notFound(async (error, reqCtx) => { + logger.error('Unable to get todo', { error }); + + return { + statusCode: HttpErrorCodes.IM_A_TEAPOT, + body: "I'm a teapot!", + headers: { + 'x-correlation-id': reqCtx.request.headers.get('x-correlation-id'), + }, + }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/gettingStarted_throwing_http_errors.ts b/examples/snippets/event-handler/rest/gettingStarted_throwing_http_errors.ts new file mode 100644 index 0000000000..417a9a2e8b --- /dev/null +++ b/examples/snippets/event-handler/rest/gettingStarted_throwing_http_errors.ts @@ -0,0 +1,23 @@ +declare function isAuthenticated(token: string): boolean; + +import { + Router, + UnauthorizedError, +} from '@aws-lambda-powertools/event-handler/experimental-rest'; +import type { Context } from 'aws-lambda'; + +const app = new Router(); + +app.use(async (_, reqCtx, next) => { + if (!isAuthenticated(reqCtx.request.headers.get('Authorization') ?? '')) { + throw new UnauthorizedError(); // This will return a 401 Unauthorized response + } + await next(); +}); + +app.get('/secure', async () => { + return { message: 'super important data' }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/rest/samples/advanced_binary_req.json b/examples/snippets/event-handler/rest/samples/advanced_binary_req.json new file mode 100644 index 0000000000..021832523b --- /dev/null +++ b/examples/snippets/event-handler/rest/samples/advanced_binary_req.json @@ -0,0 +1,9 @@ +{ + "headers": { + "Accept": "image/png", + "Accept-Encoding": "gzip" + }, + "resource": "/logo", + "path": "/logo", + "httpMethod": "GET" +} \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/samples/advanced_binary_res.json b/examples/snippets/event-handler/rest/samples/advanced_binary_res.json new file mode 100644 index 0000000000..371248a1a8 --- /dev/null +++ b/examples/snippets/event-handler/rest/samples/advanced_binary_res.json @@ -0,0 +1,13 @@ +{ + "body": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjU2cHgiIGhlaWdodD0iMjU2cHgiIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICAgIDx0aXRsZT5BV1MgTGFtYmRhPC90aXRsZT4KICAgIDxkZWZzPgogICAgICAgIDxsaW5lYXJHcmFkaWVudCB4MT0iMCUiIHkxPSIxMDAlIiB4Mj0iMTAwJSIgeTI9IjAlIiBpZD0ibGluZWFyR3JhZGllbnQtMSI+CiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiNDODUxMUIiIG9mZnNldD0iMCUiPjwvc3RvcD4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0ZGOTkwMCIgb2Zmc2V0PSIxMDAlIj48L3N0b3A+CiAgICAgICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDwvZGVmcz4KICAgIDxnPgogICAgICAgIDxyZWN0IGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQtMSkiIHg9IjAiIHk9IjAiIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2Ij48L3JlY3Q+CiAgICAgICAgPHBhdGggZD0iTTg5LjYyNDExMjYsMjExLjIgTDQ5Ljg5MDMyNzcsMjExLjIgTDkzLjgzNTQ4MzIsMTE5LjM0NzIgTDExMy43NDcyOCwxNjAuMzM5MiBMODkuNjI0MTEyNiwyMTEuMiBaIE05Ni43MDI5MzU3LDExMC41Njk2IEM5Ni4xNjQwODU4LDEwOS40NjU2IDk1LjA0MTQ4MTMsMTA4Ljc2NDggOTMuODE2MjM4NCwxMDguNzY0OCBMOTMuODA2NjE2MywxMDguNzY0OCBDOTIuNTcxNzUxNCwxMDguNzY4IDkxLjQ0OTE0NjYsMTA5LjQ3NTIgOTAuOTE5OTE4NywxMTAuNTg1NiBMNDEuOTEzNDIwOCwyMTMuMDIwOCBDNDEuNDM4NzE5NywyMTQuMDEyOCA0MS41MDYwNzU4LDIxNS4xNzc2IDQyLjA5NjI0NTEsMjE2LjEwODggQzQyLjY3OTk5OTQsMjE3LjAzNjggNDMuNzA2MzgwNSwyMTcuNiA0NC44MDY1MzMxLDIxNy42IEw5MS42NTQ0MjMsMjE3LjYgQzkyLjg5NTcwMjcsMjE3LjYgOTQuMDIxNTE0OSwyMTYuODg2NCA5NC41NTM5NTAxLDIxNS43Njk2IEwxMjAuMjAzODU5LDE2MS42ODk2IEMxMjAuNjE3NjE5LDE2MC44MTI4IDEyMC42MTQ0MTIsMTU5Ljc5ODQgMTIwLjE4NzgyMiwxNTguOTI4IEw5Ni43MDI5MzU3LDExMC41Njk2IFogTTIwNy45ODUxMTcsMjExLjIgTDE2OC41MDc5MjgsMjExLjIgTDEwNS4xNzM3ODksNzguNjI0IEMxMDQuNjQ0NTYxLDc3LjUxMDQgMTAzLjUxNTU0MSw3Ni44IDEwMi4yNzc0NjksNzYuOCBMNzYuNDQ3OTQzLDc2LjggTDc2LjQ3NjgwOTksNDQuOCBMMTI3LjEwMzA2Niw0NC44IEwxOTAuMTQ1MzI4LDE3Ny4zNzI4IEMxOTAuNjc0NTU2LDE3OC40ODY0IDE5MS44MDM1NzUsMTc5LjIgMTkzLjA0MTY0NywxNzkuMiBMMjA3Ljk4NTExNywxNzkuMiBMMjA3Ljk4NTExNywyMTEuMiBaIE0yMTEuMTkyNTU4LDE3Mi44IEwxOTUuMDcxOTU4LDE3Mi44IEwxMzIuMDI5Njk2LDQwLjIyNzIgQzEzMS41MDA0NjgsMzkuMTEzNiAxMzAuMzcxNDQ5LDM4LjQgMTI5LjEzMDE2OSwzOC40IEw3My4yNzI1NzYsMzguNCBDNzEuNTA1Mjc1OCwzOC40IDcwLjA2ODM0MjEsMzkuODMwNCA3MC4wNjUxMzQ0LDQxLjU5NjggTDcwLjAyOTg1MjgsNzkuOTk2OCBDNzAuMDI5ODUyOCw4MC44NDggNzAuMzYzNDI2Niw4MS42NjA4IDcwLjk2OTYzMyw4Mi4yNjI0IEM3MS41Njk0MjQ2LDgyLjg2NCA3Mi4zODQxMTQ2LDgzLjIgNzMuMjM3Mjk0MSw4My4yIEwxMDAuMjUzNTczLDgzLjIgTDE2My41OTA5MiwyMTUuNzc2IEMxNjQuMTIzMzU1LDIxNi44ODk2IDE2NS4yNDU5NiwyMTcuNiAxNjYuNDg0MDMyLDIxNy42IEwyMTEuMTkyNTU4LDIxNy42IEMyMTIuOTY2Mjc0LDIxNy42IDIxNC40LDIxNi4xNjY0IDIxNC40LDIxNC40IEwyMTQuNCwxNzYgQzIxNC40LDE3NC4yMzM2IDIxMi45NjYyNzQsMTcyLjggMjExLjE5MjU1OCwxNzIuOCBMMjExLjE5MjU1OCwxNzIuOCBaIiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==", + "multiValueHeaders": { + "Content-Type": [ + "image/png" + ], + "Content-Encoding": [ + "gzip" + ] + }, + "isBase64Encoded": true, + "statusCode": 200 +} \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/samples/advanced_compress_req.json b/examples/snippets/event-handler/rest/samples/advanced_compress_req.json new file mode 100644 index 0000000000..ed7979c17f --- /dev/null +++ b/examples/snippets/event-handler/rest/samples/advanced_compress_req.json @@ -0,0 +1,8 @@ +{ + "headers": { + "Accept-Encoding": "gzip" + }, + "resource": "/todos/1", + "path": "/todos/1", + "httpMethod": "GET" +} \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/samples/advanced_compress_res.json b/examples/snippets/event-handler/rest/samples/advanced_compress_res.json new file mode 100644 index 0000000000..c05f566de1 --- /dev/null +++ b/examples/snippets/event-handler/rest/samples/advanced_compress_res.json @@ -0,0 +1,13 @@ +{ + "statusCode": 200, + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ], + "Content-Encoding": [ + "gzip" + ] + }, + "body": "H4sIAAAAAAACE42STU4DMQyFrxJl3QXln96AMyAW7sSDLCVxiJ0Kqerd8TCCUOgii1EmP/783pOPXjmw+N3L0TfB+hz8brvxtC5KGtHvfMCIkzZx0HT5MPmNnziViIr2dIYoeNr8Q1x3xHsjcVadIbkZJoq2RXU8zzQROLseQ9505NzeCNQdMJNBE+UmY4zbzjAJhWtlZ57sB84BWtul+rteH2HPlVgWARwjqXkxpklK5gmEHAQqJBMtFsGVygcKmNVRjG0wxvuzGF2L0dpVUOKMC3bfJNjJgWMrCuZk7cUp02AiD72D6WKHHwUDKbiJs6AZ0VZXKOUx4uNvzdxT+E4mLcMA+6G8nzrLQkaxkNEVrFKW2VGbJCoCY7q2V3+tiv5kGThyxfTecDWbgGz/NfYXhL6ePgF9PnFdPgMAAA==", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/samples/advanced_cors_per_route.json b/examples/snippets/event-handler/rest/samples/advanced_cors_per_route.json new file mode 100644 index 0000000000..12570fedfe --- /dev/null +++ b/examples/snippets/event-handler/rest/samples/advanced_cors_per_route.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "headers": { + "access-control-allow-origin": "*", + "content-type": "application/json" + }, + "body": "{\"status\":\"ok\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/samples/advanced_cors_simple.json b/examples/snippets/event-handler/rest/samples/advanced_cors_simple.json new file mode 100644 index 0000000000..6980c59567 --- /dev/null +++ b/examples/snippets/event-handler/rest/samples/advanced_cors_simple.json @@ -0,0 +1,27 @@ +{ + "statusCode": 200, + "headers": { + "access-control-allow-credentials": "false", + "access-control-allow-origin": "https://example.com", + "content-type": "application/json" + }, + "multiValueHeaders": { + "access-control-allow-headers": [ + "Authorization", + "Content-Type", + "X-Amz-Date", + "X-Api-Key", + "X-Amz-Security-Token" + ], + "access-control-allow-methods": [ + "DELETE", + "GET", + "HEAD", + "PATCH", + "POST", + "PUT" + ] + }, + "body": "{\"todoId\":\"123\",\"task\":\"Example task\",\"completed\":false}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/samples/advanced_error_debug.json b/examples/snippets/event-handler/rest/samples/advanced_error_debug.json new file mode 100644 index 0000000000..a7b6ccaf54 --- /dev/null +++ b/examples/snippets/event-handler/rest/samples/advanced_error_debug.json @@ -0,0 +1,9 @@ +{ + "statusCode": 500, + "error": "Internal Server Error", + "message": "Actual error message from the exception", + "stack": "Full stack trace of the error", + "details": { + "errorName": "Name of the error class" + } +} \ No newline at end of file diff --git a/examples/snippets/event-handler/rest/samples/advanced_error_prod.json b/examples/snippets/event-handler/rest/samples/advanced_error_prod.json new file mode 100644 index 0000000000..7a3a12425d --- /dev/null +++ b/examples/snippets/event-handler/rest/samples/advanced_error_prod.json @@ -0,0 +1,5 @@ +{ + "statusCode": 500, + "error": "Internal Server Error", + "message": "Internal Server Error" +} \ No newline at end of file From bf736807e6e7b78f44d432d125bcd39165c8e146 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 22 Sep 2025 10:16:22 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Stefano Vozza --- docs/features/event-handler/rest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/event-handler/rest.md b/docs/features/event-handler/rest.md index 349739bdc8..18ec89cad4 100644 --- a/docs/features/event-handler/rest.md +++ b/docs/features/event-handler/rest.md @@ -144,7 +144,7 @@ You can use the `notFound()` method as a higher-order function or class method d ### Error handling -You can use the `errorHandler()` method as higher-order function or class method decorator to define a custom error handler for errors thrown in your route handlers or middleware. +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. This allows you to catch and return custom error responses, or perform any other error handling logic you need. @@ -553,7 +553,7 @@ Please [check this issue](https://github.com/aws-powertools/powertools-lambda-ty !!! note "Coming soon" -As applications grow and the number of routes a Lambda function handles increases, it becomes natural to either break into smaller Lambda functions or split routes into separate files to ease maintenance. +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.