Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

RFC: Event Handler router #2409

Closed
1 of 2 tasks
dreamorosi opened this issue Apr 22, 2024 · 4 comments
Closed
1 of 2 tasks

RFC: Event Handler router #2409

dreamorosi opened this issue Apr 22, 2024 · 4 comments
Assignees
Labels
discussing The issue needs to be discussed, elaborated, or refined RFC Technical design documents related to a feature request

Comments

@dreamorosi
Copy link
Contributor

dreamorosi commented Apr 22, 2024

Is this related to an existing feature request or issue?

#413

Which Powertools for AWS Lambda (TypeScript) utility does this relate to?

Other

Summary

This RFC aims at surveying popular routing libraries present in the Node.js ecosystem and create a decision record on whether the upcoming Powertools for AWS Lambda (TypeScript) Event Handler utility should take one of them as a dependency or implement its own routing.

Use case

Powertools for AWS Lambda is looking at implementing an Event Handler utility to work with Lambda functions triggered via Amazon API Gateway REST and HTTP APIs, Application Load Balancer (ALB), Lambda Function URLs, VPC Lattice, AWS AppSync, and later on possibly also Agents for Amazon Bedrock.

At the core of the utility there will be a router, which should be able to handle different types of sources and modes while also be optimized for Lambda, as per our tenets.

The ecosystem has a number of routing libraries with varying degrees of popularity and functionalities and at this stage it's unclear whether we should build on top of any of them or instead start from scratch and create an optimized implementation which might take inspiration from multiple of them.

Proposal

The purpose of this RFC is to survey available routing libraries present in the Node.js ecosystem, and come to a decision on whether the upcoming Powertools for AWS Lambda (TypeScript) Event Handler utility should take one of them as a dependency or implement its own.

Criteria

To keep this exercise focused, I have included only libraries that fit under these requirements:

  • Low number of runtime dependencies - this helps limit future maintenance overhead via transitive dependencies
  • Fully typed - the library should either be written in TypeScript or ship with type definitions
  • Event driven first - the library should not expect a socket to be opened
  • Project health and sustainability

Additionally, even though it doesn't constitute a disqualifying factor in itself, I also considered the library's alignment with Powertools for AWS Lambda tenets and desired DX.

Existing libraries

Because of the above, criteria I have settled on two candidates: itty-router (26K weekly downloads) and hono (287K weekly downloads).

All other routing libraries like express (27M weekly downloads), fastify (1.5M weekly downloads) even though wildly more popular have been excluded because they don’t fulfil one or more requirements. For example, both express and fastify require a socket to be listening on a certain port to accept requests and they both list a high number of runtime dependencies - respectively 31 and 16 at the time of writing.

Itty Router

An ultra-tiny API microrouter, for use when size matters (e.g. Cloudflare Workers)

The primary goal for the project, according to the documentation, is to keep a small size while offering base features beyond routing like cors, handling of several content types, and response manipulation.

Itty Router comes with three routers which build on top of each others and range from simple routing to a more complete handling of the request lifecycle via middleware, response formatter, and error handlers. All three routers appear to use the same linear loop-based RegExp routing - aka match the request with each route defined until one is found.

Hello World example:

import { AutoRouter } from 'itty-router'

const router = AutoRouter()

router
  .get('/hello/:name', ({ name }) => `Hello, ${name}!`)
  .get('/json', () => ({ foo: 'bar' }))

export default router
Hono

A small, simple, and ultrafast web framework for the Edges

According to their documentation, Hono was created primarily for Cloudflare Workers and focuses on Web Standard APIs which allows it to work with Deno and Bun runtimes. This focus on standards allows it to use objects like Request, Response, Headers that are now part of Node.js and that also help to keep size small.

Hono comes with five routers and the difference between them lies in the algorithm used to match a request with the routes defined in the application. The main router is called RegExpRouter and instead of using linear loops it flattens the regular expressions for the routes so that at the time of matching there’s only one or few regular expressions to test against the request. This results in a much faster routing with a slight penalty cost for registering routes and the inability to use certain patterns in the route definition.

The tradeoffs for the main router are mitigated by offering other routers that range from a Trie-tree based one to more conventional pattern and linear based ones. Hono also offers a SmartRouter that automatically chooses the best one between RegExpRouter and TrieRouter based on the routes defined in the application.

In addition to these many routers Hono also comes with a number of helpers and middlewares to handle cookies, cors, jwt, compression and more.

Hello World example:

import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'

const app = new Hono()

app.get('/hello/:name', (c) => c.text(`Hello ${c.req.param('name')}!`))
app.get('/json', (c) => c.json({ foo: 'bar' }))

export const handler = handle(app)

Considerations

Both libraries offer a compelling story when strictly speaking about routing. Looking at the codebases for each one, both of them appear to offer the routers as a self-contained modules which can be used independently from the application & request handling.

Neither of them publishes the routers as separate npm packages but both of them use subpath exports, which means they can be used as standalone without bundle size suffering from it - i.e. import { RegExpRouter } from 'hono/router/reg-exp-router'; or import { AutoRouter } from 'itty-router/AutoRouter';.

In both cases AWS Lambda as a target is not a primary concern and between the two Hono is the only one who ships with a ready-made adapter (hono/aws-lambda). This adapter is a thin-layer takes the Lambda handler’s event and context objects and arranges them into the shape and format expected by Hono itself and then runs the logic to match the route and call the handler.

Of the two routers, Itty Router seems to be the most opinionated and loose when it comes to patterns. For example, there’s no functional distinction between route handlers and middlewares and mutating the request object is encouraged as a mean to maintain context among handlers and middlewares.

Hono on the other hand follows a more conventional and structured approach while still being opinionated. Additionally, it offers a support for Zod validators via first-party middleware as well as support for OpenAPI components via 3rd party middleware (albeit from the same author).

When it comes to runtime dependencies, both routers have no dependencies and a small footprint. Both libraries are licensed as MIT and both of them ship hybrid CJS and ESM just like us. Neither of them publishes attestation and provenance in their npmjs.com artifacts.

Proposal

Considering Powertools for AWS Lambda maintenance policy, as well as its focus on feature parity and cohesive product vision there are inevitable tradeoffs and risks to discuss.

When it comes to Developer Experience (DX) and APIs, even though the topic itself is out of scope for this RFC, it's important to note for the sake of this argument that the goal for an Event Handler utility in Powertools for AWS Lambda (TypeScript) is to align as much as possible with the feature set and API found in the Powertools for AWS Lambda (Python) implementation, while still being idiomatic to the Node.js and TypeScript ecosystems.

In other words, one of the main design goal for Event Handler in Powertools for AWS Lambda (TypeScript) is to create a lightweight, performant, and idiomatic event router that feels natural in Lambda and seamlessly allows to work with any of the request-based services that can trigger functions (i.e. API Gateway, Function URL, ALB, etc.).

For this reason, based on my experience, I don't think adopting Itty Router or Hono wholesale is an option here. Because we want to have full control over the DX, building a layer on top of Hono for example would maybe save us some time in the very short term but quickly become an obstacle in the long term due to having to jump over backwards to reconcile our DX with the one Hono brings.

Going one layer deeper, there's an option to adopt exclusively the routing logic (i.e. RegExpRouter from Hono) and build on top it, for example by doing something like this:

import { RegExpRouter } from 'hono/router/reg-exp-router';

class ApiGatewayResolver() {
  public constructor() {
    this.router = new RegExpRouter();
  }
  
  public get({ method, path, handler }) {
    this.router.add('GET', '/hello', handler);
  }
  
  public handle(event, _context) {
    this.router.match(event.method, event.path);
  }
}

This however, even though it won't incur a bundle penalty for those customers bundling their functions (i.e. with esbuild), raises other concerns that are instead more routed into governance.

Powertools for AWS Lambda versioning policy means that at any point in time we are supporting 3 to 4 Node.js major versions depending on the currently active AWS Lambda managed runtimes. Of these, in practice, half or more are usually in End-of-Life or Maintenance stage for over half of the time we have to support them.

Other OSS libraries however don’t have this type of constraint and so they can afford to drop support for a Node.js version as soon as it enters the Maintenance stage. This represents a risk for us due to the possibility of them introducing runtime/language features that are not backwards compatible and thus potentially cutting us and our customers off newer versions and security releases.

This is dynamic is very similar to the one we are already experiencing with Middy.js (#2049) and that over the years has caused confusion and friction. Luckily, in Middy.js’ case the breaking changes have been close to none and there hasn’t been any vulnerability that we were not able to address due to us not being able to support a more recent version.

For a dependency concerned with request handling and that deals with RegExp routing however the risk surface area is significantly bigger and so I believe we should be much more careful with the decision and strongly consider being in charge of our destiny.

Customers have shared with us a number of times that one of the things they appreciate the most in Powertools is that we take on complexity and simplify their supply chain. This is especially important for certain categories of customers that are more sensible to this type of risk and that also happen to be a seizable cohort of Powertools customers.

Note

To sum up, I propose to move forward with our own implementation and recommend against adopting either of the two libraries as dependency at this stage.

A note on Hono

This is not an easy decision, especially because I personally like Hono and its API is undoubtably very most promising. In its current form, Hono aligns with many of our design goals in terms of being lightweight and bringing a delightful and simple experience. However its clear alignment with Cloudflare Workers both in technical and governance terms represents too big of a risk for the long term sustainability of the integration for me to ignore.

Thanks to its wide range of adapters, middlewares, and 3rd-party integrations we will still recommend Hono to all these customers who are looking for an isomorphic router that can be used not only in AWS Lambda but also and especially on other platforms.

Additionally, because of its API and great performance benchmarks, I can already expect that we will continue looking at Hono for lessons learned or patterns that can benefit our customers for our own implementation.

Out of scope

The RFC will take in account the use cases that the utility will have to cover and might include code snippets for the sake of clarify, however designing the DX/UX of the utility itself is out of scope and any code example brought here might not be used for the final RFC/implementation.

Potential challenges

The main challenges at this stage are:

  • foreseeing the needs of an utility that hasn't been designed
  • predicting the roadmap and health of 3rd party dependencies

The first point can be mitigated by looking at the feature set and DX of the Powertools for AWS Lambda (Python) Event Handler. The second one is harder to mitigate.

Dependencies and Integrations

No response

Alternative solutions

No response

Acknowledgment

Future readers

Please react with 👍 and your use case to help us understand customer demand.

@dreamorosi dreamorosi added RFC Technical design documents related to a feature request discussing The issue needs to be discussed, elaborated, or refined labels Apr 22, 2024
@dreamorosi dreamorosi self-assigned this Apr 22, 2024
@dreamorosi
Copy link
Contributor Author

I have updated the RFC after researching some of the most popular routing libraries in the ecosystem.

Ultimately I believe we should implement our own routing algorithm(s) and build on top of them so that we can avoid some future friction and have full control over the DX, even if this means taking on a bigger backlog.

@heitorlessa
Copy link
Contributor

Firstly, I can't thank you enough for the thorough investigation, and great write up.

I agree with the direction: build a tiny layer ourselves. Here are my thoughts on why that from our experience in Python:

  • Institutional knowledge. It takes a few years for developers to become experts in a given framework, syntax, and ecosystem. For large companies, betting on frameworks makes it easier to have a similar mindset and onboarding, plus knowledge transfer. It doesn't always means it's a better choice, but stability and familiarity play an important role in our every day choices. Event Handler solves the primary problem that our customers told us over the years: capitalize institutional knowledge while not sacrificing performance and stability in modern environments like AWS Lambda. That said, we must stay honest on what our recommended option is for non-AWS Lambda. Make no mistake, some customers will try ports & adapters and run the same app in both Lambda and Fargate, or eventually move to another compute platform. If and when that happens, our investment in familiarity pays off for our customers' long term success.

  • Event Sources differences add up. At first, it looks like a routing problem - take GET /todos/{todoId} - and after a few days you now have custom domain mappings, CORS, single vs multi-header, single vs multi-query strings, etc. The faux start to build on top of a giant's shoulders can lead to a plethora of papercuts, which slowly but surely you created another set of routers on top of them. Excluding infrastructure limits we can't control, we can handle these differences with a tiny router ourselve, followed by specialized serializers/deserializers. The biggest hurdle comes with OpenAPI - if Zod or similar aren't much of a big deal to support, then it's less of a concern. Building our own adds the possibility to handle our next step with Event Handler - route any event from any source along with their convenient utilities e.g., route EventBridge events, standardize events, deduplication, batching, etc.

We're still missing a mechanism to wrap up feature design to make this seamless for you folks. Transparently, we thought about building in Rust, but serving multi-platform binaries in NPM, Maven, and NuGet wasn't a done deal supply chain wise. Reach out if we can help with anything. We've got a shopping list of things I wish we've done differently.. starting with globals, and not centering data validation much earlier.

@dreamorosi
Copy link
Contributor Author

I will mark this first RFC as closed and signed off by @heitorlessa.

My next step is to start working on the main one.

Copy link
Contributor

⚠️ COMMENT VISIBILITY WARNING ⚠️

This issue is now closed. Please be mindful that future comments are hard for our team to see.

If you need more assistance, please either tag a team member or open a new issue that references this one.

If you wish to keep having a conversation with other community members under this issue feel free to do so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussing The issue needs to be discussed, elaborated, or refined RFC Technical design documents related to a feature request
Projects
Development

No branches or pull requests

2 participants