Skip to content
On this page

Middleware

Lamware is built around the idea of Middleware being used to modify and transform a function. Middleware comes down to being a plain object, which can optionally define various hooks and wrappers.

Official Middleware

We maintain some basic and useful Middleware over in our GitHub repo.

Third-Party Middleware

Hmm

We don't have anything here yet. Want to add your own middleware? Open a PR!

Developing Middleware

Middleware is, at its core, just an object. You can use the Middleware type from @lamware/core to ensure things are typed correctly.

One important thing to note is that, whilst not required, providing a Handler type to the Middleware interface is recommended. It will properly type your hooks, as well as providing a type error should someone try to use your Middleware on an unsupported type.

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>> => ({
  id: 'my-middleware',
});

Alternatively, you can define middleware in-line with the use() function:

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';
import { lamware } from '@lamware/core';

const { handler } = lamware<APIGatewayProxyHandlerV2<any>>()
  .use<Middleware<APIGatewayProxyHandlerV2<any>>>({
    id: 'my-middleware',
  })
  .execute(async () => {
    return { statusCode: 200 };
  });

export { handler };

Hooks

Lamware provides three runtime hooks:

  • before - Runs before the Handler is executed.
  • after - Runs after the Handler is executed.
  • uncaughtException - Runs if an uncaught exception happens during before, after or the Handler itself. Useful for error/crash reporting, cleaning up after a failure, etc.
typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>> => ({
  id: 'my-middleware',
  before: async (payload) => {
    // You can set state (see below)
    payload.state = { helloWorld = '123' };

    // Or return a response and exit the function early.
    payload.response = { statusCode: 401 };

    // You can also modify the function event and context.
    payload.event.rawPath = '/hello-world';
    payload.context.debug = true;

    return payload;
  },
  after: async (payload) => {
    // The response here is from the function itself, and you can modify it.
    payload.response.statusCode = 200;

    return payload;
  },
  uncaughtException: async ({ context, cause }) => {
    /**
     * See "Handling Errors" below!
     */
  },
});

State

Middleware is stateless, other than modifying its own custom state object, which will be merged in to the state used by the function. You can provide typings for this state, which will also be merged and used in the function handler.

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';

interface State {
  helloWorld: string;
}

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>, State> => ({
  id: 'my-middleware',
  before: async (payload) => {
    payload.state = {
      helloWorld = '123',
    };

    return payload;
  },
});

Initialization

Middleware can provide an init function which will be called before the Lambda function is executed. This is useful for memoizing instances in to state, fetching data from an API, or any other asynchronous operation.

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';

interface State {
  helloWorld: string;
}

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>, State> => ({
  id: 'my-middleware',
  init: async () => {
    // Pull data from API etc.

    // You can also return your initial state here.
    return {
      helloWorld: '123',
    };
  },
});

Handling Errors

As mentioned previously, Lamware has an uncaughtException hook. If provided, this will be executed whenever an uncaught exception occurs within:

  • before hooks
  • after hooks
  • execute() handler

You can use this to log errors, handle clean-up and anything else that you might want to do if all else fails.

Note: If an uncaught exception happens during the uncaughtException hook, it will be thrown top-level. It's important to catch these yourself if you don't want anything to potentially return an error for the Lambda Function. For example, if you report errors to an external service via uncaughtException, wrap in a try/catch as the external service may go down.

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>> => ({
  id: 'my-middleware',
  uncaughtException: async (payload) => {
    const { exception } = payload.cause;

    /**
     * `exception` is the `Error` instance that caused the
     * `uncaughtException` hook to run.
     */
    await sendToSentry(exception);

    /**
     * You should also provide a response. By default it will
     * be an empty object, which will break for APIG v1/v2.
     */
    payload.response = {
      statusCode: 200,
      body: JSON.stringify({ hello: 'world' }),
    };

    return payload;
  },
});

Wrapping the Handler

Sometimes you need to wrap your Handler, for example when using Sentry to allow it to capture handler errors. Through a Lamware middleware, you can provide a wrapper function that will automatically be used for the executed handler.

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>> => ({
  id: 'my-middleware',
  wrap: handler => handler,
});

In the cases where, for example Sentry, you have to provide a regular handler ((event, context, callback) => {}) instead of the object-based Lamware one, you can use our wrapCompat function to wrap using the compatibility wrapper:

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';
import { AWSLambda } from '@sentry/serverless';
import { wrapCompat } from '@lamware/core';

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>> => ({
  id: 'my-middleware',
  wrap: handler => wrapCompat(handler, compatHandler => {
    return AWSLambda.wrapHandler(compatHandler, {
      captureTimeoutWarning: false,
      rethrowAfterCapture: true,
      callbackWaitsForEmptyEventLoop: false,
      ...(config?.wrapper ?? {}),
    });
  }),
});

Filter

Sometimes you may want your Middleware to not be registered, even if the user has added it via .use(). To achieve this, you can implement the synchronous filter() function:

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>> => ({
  id: 'my-middleware',
  filter: () => {
    return false; // Returning `false` will prevent the Middleware being registered.
  },
});

Middleware Purity

When running Middleware hooks, Lamware will run them in parallel. This improves performance, but should your Middleware modify the state in some way (i.e. changing the Lambda context or anything else in the payload) you should tell Lamware that the Middleware isn't pure.

By telling Lamware that your Middleware isn't pure, it will be sure to run it alone. The order in which Middleware hooks are executed is based on the order the Developer registers them in via .use(), so if your impure Middleware is registered between two pure Middlewares, it will break the chain and run your impure Middleware inbetween them.

You can do this via the pure property of Middleware:

typescript
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import type { Middleware } from '@lamware/core';

const myMiddleware = (): Middleware<APIGatewayProxyHandlerV2<any>> => ({
  id: 'my-middleware',
  pure: false, // `true` by default.
});

GPL-3.0 Licensed