RFC: Event Handler for REST APIs #3500
Replies: 9 comments 24 replies
-
Since this is a high-level definition of the implementation and features, I have nothing to add other than adding a
Congrats on writing this RFC @dreamorosi! It's going to be the most anticipated utility for Typescript Powertools for this year. |
Beta Was this translation helpful? Give feedback.
-
Looks fab and I can't wait to start using this for real to replace a less elegant solution we currently have in place! Could I get clarification on a couple of bits?
|
Beta Was this translation helpful? Give feedback.
-
Is there a reason that const app = APIGatewayResolver();
const todosRouter = new Router(); Personally, I prefer factory functions that don't use
Am I missing something but I don't see a import { APIGatewayResolver } from '@aws-lambda-powertools/event-handler/resolvers';
const app = APIGatewayResolver();
app.post('/todos/', () => {
const data = JSON.parse(app.currentEvent.body || '{}');
const todos = await fetch(`https://jsonplaceholder.typicode.com/todos`, {
body: JSON.stringify(data),
});
return {
todo: await todos.json(),
};
}, {
method: ['PUT', 'POST'] // <= http methods
}); On another point, it seems a bit strange to me that the name of the method here is Also, what happens if some someone does this: app.post('/todos/', () => {
// ...
}, {
method: ['PUT', 'POST']
});
app.post('/todos/', () => {
// ...
}, {
method: ['POST']
}); Which |
Beta Was this translation helpful? Give feedback.
-
BloatI think the APIGWIt also isn't clear if the routes in api gateway APIGW ANY SupportI didn't see any callout for APIGW ANY, is this going to be supported automatically, or are we going to have to know about it? For instance, if we want a route agnostic handler, is that possible? 405 Method Not AllowedHow will this be supported? How will we know that the method isn't allowed, in some frameworks we need to have brittle duplicated code that contains the same list of supported methods in order to report the 405, and then duplicate this in every router. The framework we are using right now automatically generates 405s for us without any complexity. AuthorizersI didn't see any support for custom lambda authorizers which almost always are necessary for us v2 HTTP APIsWe heavily rely upon v2 HTTP APIs and not REST APIs, is this RFC meant for both or only specifically the v1 REST APIs? Access to the templated pathIt wasn't clear if we need to access metadata about the powertools routed event how we would do that. the 405s issue is one of them, another one is getting both resolved MiddlewaresWe absolutely need a way to intercept all requests:
Integration with Express, Hono, Middy, etc...How exactly can we use this to debug a running HTTP version of our service? Is there some way to export the stored data so that it can be injected into these HTTP frameworks? I don't expect understanding of these frameworks or even an integration, but rather, a method like |
Beta Was this translation helpful? Give feedback.
-
One feature I like about |
Beta Was this translation helpful? Give feedback.
-
I appreciate the effort behind this RFC, but I strongly disagree with the lambdalith approach it seems to promote. Consolidating multiple routes with distinct Swagger specs into a single Lambda goes against serverless best practices and introduces significant operational and security challenges. Key Concerns: While lambdaliths may appear to simplify routing, allow for the ability of having a better-defined OpenAPI spec and reduce cold starts, the trade-offs in performance, security, and maintainability outweigh the benefits. Serverless succeeds because of its modularity and principle of least privilege. Let’s focus on patterns that enhance these strengths rather than undermine them. Would love to hear others’ thoughts on maintaining best practices while achieving the goals this RFC targets. |
Beta Was this translation helpful? Give feedback.
-
For the Open API section where you detail customizing parameters: is the intention that each of those parameters will be top level attributes to the config object? I worry that there's quite a few parameters, and as the Open API spec changes you might open yourself up to name collisions. Thoughts on scoping the customizations to an attribute like Awesome work with the RFC, keen to see what's next. Will there be much opportunity for community contributions to this work? |
Beta Was this translation helpful? Give feedback.
-
Hey @dreamorosi and team! Love the effort here - well-written, structured, thought through - fantastic work as always! Can you expand a little on the proposal to access I.e. current [...]
const app = new APIGatewayResolver();
app.get('/search', () => {
const query = app.currentEvent.query('q');
const { limit, offset } = app.currentEvent.query();
return {
message: 'Hello, World!'
};
});
[...] vs something like [...]
const app = new APIGatewayResolver();
app.get('/search', ({ currentEvent }) => {
const query = currentEvent.query('q');
const { limit, offset } = currentEvent.query();
return {
message: 'Hello, World!'
};
});
[...] |
Beta Was this translation helpful? Give feedback.
-
What would be the advantages of having this while libraries like Hono for Lambda already do that very well? |
Beta Was this translation helpful? Give feedback.
-
Is this related to an existing feature request or issue?
#413
Which area does this RFC relate to?
Event Handler
Summary
Resolvers handle request resolution, including one or more routers, and give access to the current event via typed properties.
While the code samples in the RFC will use mainly
APIGatewayRestResolver
, the first iteration of the feature will have in scope also theAPIGatewayHttpResolver
andLambdaFunctionUrlResolver
resolvers. Other resolvers present in the Python version of Powertools for AWS will be added in subsequent releases and based on demand.In terms of patterns of usage, Powertools for AWS Lambda (TypeScript) generally supports three patterns:
The first iteration of the feature will include only 1/ and 2/, while Middy.js middleware usage will be added later if there's demand.
Use case
Powertools for AWS customers have been asking us to provide a utility similar to the one present in Powertools for AWS Lambda (Python) that allows them to easily build REST APIs backed by an AWS Lambda function.
While there are already many popular frameworks in the Node.js ecosystem such as Express.js, Fastify, and Hono, none of them targets specifically and uniquely Lambda customers, and many enterprise customers who have already adopted Powertools for AWS in their workloads, would prefer a solution delivered by us rather than relying on a 3rd party dependency.
For this reason we are continuing our work to improve feature parity between Powertools for AWS versions and will offer an Event Handler utility in this version. We don't necessarily expect customers who have had success and are happy with other frameworks to migrate to Event Handler; this utility is rather for those who appreciate our commitment to supply chain security and our efforts to build lightweight utilities specifically focused on Lambda.
The goal of this RFC is to propose a feature set that matches the one present in Powertools for AWS Lambda (Python) while also being idiomatic with the Node.js ecosystem and adding selected features that make sense in this version.
Proposal
Response auto-serialization
For your convenience, you can return a plain object response and we will automatically:
object
(Record<k, v>
) responses to JSONContent-Type
toapplication/json
statusCode
to 200 (OK)If you want full control of the response, headers, and status code you can also return a Response object (more on this later).
Dynamic routes
You can use
/todos/:todoId
to configure dynamic URL paths, where:todoId
will be resolved at runtime.Dynamic paths can also be nested and used at different levels, i.e.
/todos/:todoId?/comments/:commentId/hidden/:isHidden
and in all cases, the parameters should be strongly typed and customers should get type hints in their IDE like this:Query Strings
Within
app.currentEvent
property, you can access all available query strings as an object viaquery
.Payload
You can access the raw payload via the
body
property, or if it's a JSON string you can quickly deserialize it via thejson()
method.Headers
Similarly to query strings, you can access headers as dictionary via
app.currentEvent.headers
. Specifically for headers, it's a case-insensitive dictionary, so all lookups are case-insensitive.Catch-all routes
You can also create things like catch-all routes, for example the .+ expression allows you to handle an arbitrary number of paths within a request.
HTTP Methods
You can use named methods to specify the HTTP method that should be handled in your functions. That is,
app.<http_method>
, where the HTTP method could beget()
,post()
,put()
,patch()
,delete()
,head()
.If you need to accept multiple HTTP methods in a single function, or support a custom HTTP method for which no method exists (e.g.
TRACE
), you can use theroute()
method and pass an array of HTTP methods.Below you'll find sections about how we handle routes not found and method not allowed, as well as how to customize the default behavior.
Data validation
All resolvers can optionally coerce and validate incoming requests by setting
enableValidation: true
.With this feature, we can now express how we expect our incoming data and response to look like. This moves data validation responsibilities to Event Handler resolvers, reducing a ton of boilerplate code.
You can pass an output schema to signal our resolver what shape you expect your data to be.
Handling validation errors
Any incoming request that fails validation will lead to a
HTTP 422: Unprocessable Entity
error response that will look similar to this:You can customize the error message by catching the
RequestValidationError
exception. This is useful when you might have a security policy to return opaque validation errors, or have a company standard for API validation errors.Here's an example where we catch validation errors, log all details for further investigation, and return the same
HTTP 422
with an opaque error.Validating payloads
You can do something similar with the incoming payload as well, in this case you can pass a schema to the
validation.input
property.We will automatically parse and validate the outer envelope of the event according to the resolver you are using, so that you can focus on providing the schema for the body only.
Enabling SwaggerUI
Since Event Handler supports OpenAPI, you can use SwaggerUI to visualize and interact with your API.
We will implement this feature as a complete opt-in feature and with as little overhead as possible. Because of this we will return the HTML needed to render the SwaggerUI but you will have to bundle your own
swagger-ui-bundle.min.js
together with your function or provide an url to a CDN-hosted version of it (i.e. this).Accessing request details
Event Handler exposes the resolver request and Lambda context under convenient properties like:
app.currentEvent
andapp.lambdaContext
, this is why you seeapp.resolve(event, context)
in every example.Handling not found routes
By default, we return
404
for any unmatched route.You can use the
notFound()
method or class method decorator to override this behavior, and return a custom response.Handling methods not allowed
By default, we return 405 for any request that matches a route but is using a method with no resolver.
You can use the
methodNotAllowed()
method or class method decorator to override this behavior, and return a custom response.Error handling
To keep your route handlers as focused as possible, you can use the
errorHandler()
method or class method decorator with anyError
class or children. This allows you to handle common errors outside of your route, for example validation errors.The
errorHandler()
method also supports passing a list of error classes you want to handle with a single handler.Internally, we’ll test the error being thrown using
error instanceof YourError
first, and thenerror.name === YourError.name
, this allows us to ensure equality even when the imports or the bundle might be polluted with double imports.Raising HTTP errors
You can easily raise any HTTP Error back to the client using one of the handy prebuilt error responses. This ensures your Lambda function doesn’t fail but return the correct HTTP response signalling the error.
We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 404, 500, etc.
Custom Domain API Mappings
When using the Custom Domain API Mappings feature in Amazon API Gateway, you must use the
stripPrefixes
parameter in theAPIGatewayRestResolver
constructor.Scenario: You have a custom domain
api.mydomain.dev
. Then you set a/payment
mapping to forward any payment requests to your Payments API.Challenge: This means you path value for any API requests will always contain
/payment/<actual_request>
, leading to HTTP 404 as Event Handler is trying to match what’s afterpayment/
. This gets further complicated with an arbitrary level of nesting.Solution: To address this, we use the
stripPrefixes
parameter to remove these prefixes before trying to match the routes you defined in your application.After removing a path prefix with
stripPrefixes
, the new root path will automatically be mapped to the path argument of/
.For example. when using the
stripPrefixes
value of/pay
, there is no difference between a request path of/pay
and/pay/
; and the path argument would be defined as just/
.Advanced use cases
CORS
You can configure CORS in each resolver constructor via the
cors
parameter with aCORSConfig
class.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.Optionally, you can disable CORS on a per path basis with the
cors: false
option.Pre-flight
Pre-flight (
OPTIONS
) calls are typically handled by API Gateway or Lambda Function URL. For ALB instead, you are expected to handle these requests yourself.For convenience, we automatically handle them for you as long as you enable CORS in the constructor.
Defaults
For convenience, these are the default values when using
CORSConfig
to enable CORS:*
*
for production unless your use case strictly requires it[]
allowOrigin
["Authorization", "Content-Type", "X-Amz-Date", "X-Api-Key", "Amz-Security-Token"]
[]
false
Compress
You can compress with gzip and base64 encode your responses via the
compress
parameter. You have the option to pass thecompress
parameter when working with a specific route.The client must send the
Accept-Encoding
header, otherwise we will send a normal response.Binary responses
For convenience, we automatically base64 encode binary responses. You can also use in combination with
compress
parameter if your client supports gzip.Similar to the
compress
feature, the client must send theAccept
header with the correct media type.Notes:
*/*
binary media type when CORS is also configuredDebug mode
You can enable debug mode via the
POWERTOOLS_DEV
orPOWERTOOLS_EVENT_HANDLER_DEBUG
environment variables.This will enable full traceback errors in the response, log any request and response, and set CORS to allow any origin (
*
).Since this might reveal sensitive information in your logs and relax CORS restrictions, we recommend you to use this only for local development and as sparingly as possible.
OpenAPI
When you enable data validation, we use a combination of standard-schema compatible schemas (Zod, Valibot, Arktypes) and OpenAPI type annotations to add constraints to your API's parameters.
In OpenAPI documentation tools like SwaggerUI, these annotations become readable descriptions, offering a self-explanatory API interface. This reduces boilerplate code while improving functionality and enabling auto-documentation.
Customizing OpenAPI parameters
Whenever you use OpenAPI parameters to validate query strings or path parameters, you can enhance validation and OpenAPI documentation by using any of these parameters:
true
, strict validation is applied to the field. See Strict Mode for detailsfalse
the field will not be part of the exported OpenAPI schemaCustomizing API operations
Customize your API endpoints by adding metadata to endpoint definitions.
Here's a breakdown of various customizable fields:
To implement these customizations, include extra parameters when defining your routes:
Split routes with Router
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
Router
feature is useful.Let's assume you have
index.ts
as your Lambda function entrypoint and routes intodos.ts
. This is how you'd use the Router feature.todos.ts
index.ts
Route prefix
When necessary, you can set a prefix when including a router object. This means you could remove
/todos
prefix altogether.todos.ts
index.ts
Specialized routers
You can use specialized router classes according to the type of event that you are resolving. This way you'll get type hints from your IDE as you access the
currentEvent
property.Sharing contextual data
You can use
appendContext
when you want to share data between your App and Router instances. Any data you share will be available via the context object available in your App or Router context.We always clear data available in context after each invocation.
index.ts
Out of scope
The following items are to be considered out of scope for the first iteration of the feature:
If you are interested in any of them and would like us to prioritize them right after the first version, please let us know under this RFC.
Potential challenges
TBD
Dependencies and Integrations
No response
Alternative solutions
Acknowledgment
Future readers
Please react with 👍 and your use case to help us understand customer demand.
Beta Was this translation helpful? Give feedback.
All reactions