npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@matt-usurp/pilgrim

v1.0.0

Published

A complete type-safe implementation of middleware and handlers for serverless functions

Downloads

25

Readme

Pilgrim

A type-safe serverless handler and middleware implementation.

Currently supporting aws-lambda natively but the internal API is abstracted enough to be able to support other providers. In version 2.x I hope to extract the providers out to their own packages for a better developer experience.

Usage

The core api is around the HandlerBuilder which focuses on mutating a context object through middlewares. This exposes a .use() function that allows for middlewares to apply mutations to the expected types given to the handler. The .handle() function finalises the chain and constructs an entrypoint for the service provider to execute.

Service provider implementations expose pre-defined instances of HandlerBuilder with types valid for that provider.

An example of an aws-lambda handler:

import { aws, response } from '@matt-usurp/pilgrim/provider/aws';

export const target = aws<'aws:apigw:proxy:v2'>()
  .handle(async ({ context }) => {
    return response.event({ ... });
  });

Which aws-lambda can execute by targetting the filename.target exported handler.

Context

A handler is provided a context object that is restricted in what is available out of the box. However it is possible to augement the context by providing implementations of Pilgrim.Middleware that access the event source or external services.

Providers may have augmented types available for assisting with making these middleware. For example, aws-lambda based middleware can make use of the Lambda.Middleware exposed by the provider implementation.

An example middleware for aws-lambda might look like:

import { Pilgrim } from '@matt-usurp/pilgrim';
import { Lambda } from '@matt-usurp/pilgrim/provider/aws';

type MyNewContext = { user: { id: string; }; };
type MyMiddleware = Lambda.Middleware<'aws:apigw:proxy:v2', Pilgrim.Inherit, MyNewContext, Pilgrim.Inherit, Pilgrim.Inherit>;

export const withUserData: MyMiddleware = async ({ source, next }) => {
  // Note, "source.event" is APIGatewayProxyEventV2 due to specifying "aws:apigw:proxy:v2"
  // Additionally, "source.context" is the Lambda function context.

  const header = source.event.headers['authorization'];
  const context = {
    ...context,

    user: {
      id: await validateUserId(header),
    },
  };

  // You must return what next returns, this is the response in the chain.
  // The context is merged and provided to the next middleware or handler.
  return next(context)
}

All middleware are asynchronous which allows them to delay the execution and wait for processes. This means you can perform tasks to resolve information and merge that with the handler context. In the example above, validateUserId() could communicate with the database to make sure the user id exists.

All middleware are provided with the next() function which allows for the chaining to work. The response of the next function is the return value of the future middleware (or the handler). If your middleware wishes too--it can return its own value and not call next(). This would be useful for cases where some validation failed and you want to return a 404 or something.

Our new middleware can be used within our example handler code above by adding a .use() call before the .handler().

import { response } from '@matt-usurp/pilgrim/provider/aws';

export const target = aws<'aws:apigw:proxy:v2'>()
  .use(withUserData)
  .handle(async ({ context }) => {
    context.user.id; // user id is now available in context

    return response.event({ ... });
  });

Response

All middleware and handlers are expected to wrap their responses. This is done so middleware can have a common discriminator to test when doing response mutations. Some generic helpers are exported from the main namespace for crafting common responses or creating your own.

For example, satisfying a nothing response instead of using void.

import { response } from '@matt-usurp/pilgrim';

response.nothing(); // Pilgrim.Response.Nothing

When creating your own responses you will also want to define a type for it. The response type helper Pilgrim.Response should be used to create wrapped responses.

import { Pilgrim, response } from '@matt-usurp/pilgrim';

type ColourResponse = Pilgrim.Response<'colours', { colours: string[]; }>;

// Creates the response.
const response = response.create<ColourResponse>('colours', { colours: ['red', 'green', 'blue'] });
response; // type ColourResponse

// Creating a factory function for constructing the response.
const factory = response.factory<ColourResponse>('colours');
const response = factory({ colours: ['orange'] });
response; // type ColourResponse

Note that @matt-usurp/pilgrim/provider/aws also exports response (the same as the main import) with some additional functions tailored for aws responses. Currently all aws responses are wrapped in an aws:event response that can be created through response.event().

Our new responses can then be used with middleware to allow it in the execution chain. This is a fairly complex topic but can be better explained by reading the aws-apigw-http-response example and viewing the withPilgrimHttpResponseSupport middleware provided in the aws module.

Middleware

Middleware can introduce new responses by specifying them in the ResponseInbound generic parameter. It is important to note, when specifying a new response you must also supply a ResponseOutput to indicate what transformation is happening within the middleware.

For example, lets create a middleware that will allow use of the custom response ColourResponse defined above.

import { Pilgrim, response } from '@matt-usurp/pilgrim';

type ColourResponseMiddleware = (
  Pilgrim.Middleware.WithoutSource<
    Pilgrim.Inherit, // we do not require context
    Pilgrim.Inherit, // we do not change context
    ColourResponse, // allowing ColourResponse inbound
    Pilgrim.Response.Http // transforming to a http response
  >
);

const middleware: ColourResponseMiddleware = async({ context, next }) => {
  const result = await next(context);

  // using the discriminator "type" to detect our colour response.
  // TS should resolve result to ColourResponse.
  if (result.type === 'colours') {
    return response.http({
      status: 200,
      body: JSON.stringify(result.value.colours);
    });
  }

  // returns any other response.
  return result;
}

This middleware can now be used (with use()) in the execution chain. All middleware and handlers next in the chain can safely return the ColourResponse. This is enforced through types and will cause build failures if the middleware is not used.

There is an "inherit" response which should be considered a pseudo response. You cannot test for "inherit" as it doesn't actually exist. This might show up in middleware's representing "any" response you have not manually typed. Simply return this response in a default block. This allows middleware to be partially aware of responses.

Further reading

  • For more examples see the /examples directory.
  • For supported events (such as aws:apigw:proxy:v2) see the /src/provider/aws/lambda/source directory. Note that this is an extensible interface so if you event is missing you can add it yourself by doing the same thing. However, do feel free to PR that back in to the project!

Future Features

  • Introduce examples for other providers such as azure and gcp.
  • Flesh out test cases to ensure all execution branches are covered.
  • Breakout documentation in to less overwhelming wall of text.
  • Breakout provider implementations