aws-powertools / powertools-lambda-typescript

Powertools is a developer toolkit to implement Serverless best practices and increase developer velocity.
https://docs.powertools.aws.dev/lambda/typescript/latest/
MIT No Attribution
1.53k stars 135 forks source link

RFC: Event Handler router #2409

Closed dreamorosi closed 3 months ago

dreamorosi commented 4 months ago

Is this related to an existing feature request or issue?

https://github.com/aws-powertools/powertools-lambda-typescript/issues/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:

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:

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 commented 3 months ago

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 commented 3 months ago

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:

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 commented 3 months ago

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

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

github-actions[bot] commented 3 months ago

⚠️ 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.