Serverless Compose (Lambda Middleware)

raw JSON →
2.4.0 verified Thu Apr 23 auth: no javascript

serverless-compose is a lightweight, functional middleware framework specifically designed for AWS Lambda functions, enabling the creation of composable, "onion-style" middleware stacks. Currently at version 2.4.0, its release cadence is not explicitly stated in the provided documentation, but it appears actively maintained. A key differentiator is its zero-dependency philosophy, ensuring a minimal bundle size and avoiding dependency conflicts. Unlike more opinionated frameworks, serverless-compose focuses on providing a thin `compose` function and flexible patterns like `recoveryMiddleware` and `timingLogMiddleware` without enforcing specific architectural designs, allowing developers to define their middleware stack with explicit, functional wrappers around their Lambda handlers. It aims to prevent the "fossilization" of bad middleware habits by promoting a clear, functional approach for event processing.

error SyntaxError: Cannot use import statement outside a module
cause Your Node.js environment or AWS Lambda configuration is treating your code as CommonJS (CJS) but it uses ESM `import` statements.
fix
Add "type": "module" to your package.json, or rename your file to .mjs, or ensure your build process (e.g., Babel, TypeScript compiler, bundler) transpiles ESM to CJS if your environment is CJS-only.
error ReferenceError: require is not defined in ES module scope, you can use import instead
cause Your Node.js environment or AWS Lambda configuration is treating your code as an ES Module (ESM), but you are attempting to use the CommonJS `require` function.
fix
Replace all require() calls with import statements for serverless-compose and other dependencies. If you need to load CJS modules from an ESM context, use await import('your-cjs-module').
error 502 Internal Server Error (from API Gateway) with errorMessage: "Unhandled Promise Rejection" in CloudWatch logs
cause Your Lambda handler or a middleware function threw an error or returned a rejected promise, and no subsequent middleware (like `recoveryMiddleware`) transformed it into a valid API Gateway response.
fix
Ensure recoveryMiddleware is included in your compose stack and its error handling function is correctly implemented to return a structured API Gateway proxy response object (e.g., { statusCode: 500, body: JSON.stringify({ message: 'Error' }) }). Place recoveryMiddleware towards the 'right' in your compose arguments to wrap all preceding middleware and your handler.
gotcha Middleware order is crucial in `serverless-compose`'s onion-style architecture. Middleware functions are applied from right to left (outermost to innermost) in the `compose` function, but execute in an 'onion' fashion (outer wraps inner). Misordering can lead to unexpected behavior or missed error handling.
fix Carefully consider the execution flow. Middleware like `recoveryMiddleware` that needs to catch errors from subsequent middleware or the handler should typically be placed earlier in the `compose` argument list (further to the right), allowing it to wrap and protect the functions composed after it.
gotcha When migrating older CommonJS (CJS) Lambda functions or integrating CJS dependencies, you might encounter issues with `serverless-compose` as it primarily uses ES Modules (ESM). AWS Lambda environments need proper configuration (`"type": "module"` in `package.json` or `.mjs` file extensions) to handle ESM correctly.
fix Ensure your project's `package.json` specifies `"type": "module"` for ESM, or use a build step (e.g., esbuild, webpack) to transpile your code to CJS if your environment strictly requires it. For hybrid setups, consider using dynamic `import()` within CJS modules to load ESM dependencies.
gotcha Unhandled promise rejections or uncaught exceptions within your Lambda handler or custom middleware can lead to generic `502 Internal Server Error` responses from API Gateway without informative bodies. `recoveryMiddleware` is designed to mitigate this, but must be configured correctly.
fix Always include and configure `recoveryMiddleware` (or similar custom error handling) as one of the outermost middleware in your `compose` stack. Ensure its error handling function returns a valid API Gateway proxy response object (e.g., `{ statusCode: 500, body: JSON.stringify({ message: 'Error' }) }`).
gotcha While the library itself is functional, complex Serverless Framework deployments using shared source directories or monorepos can suffer from path resolution and bundling issues, especially when coupled with plugins like `serverless-esbuild` or `serverless-plugin-typescript`.
fix Verify `handler` paths in `serverless.yml` are correct relative to the service's `package.json`. For monorepos, ensure your build process correctly copies or symlinks shared code, or configure bundling plugins to resolve modules from the root. Explicitly setting `srcDir` might be needed for some bundlers.
npm install serverless-compose
yarn add serverless-compose
pnpm add serverless-compose

Demonstrates basic setup with `compose`, `timingLogMiddleware` for logging duration, and `recoveryMiddleware` for graceful error handling in an AWS Lambda handler.

import { compose, recoveryMiddleware, timingLogMiddleware } from 'serverless-compose';

// Suppose this is a client that fetches the weather from some external API
class WeatherClient {
  static async askBadAPIForWeather() {
    // Simulate a failing API call 50% of the time
    if (Math.random() > 0.5) {
      throw new Error('Weather API is down!');
    }
    return { temperature: 25, unit: 'C' };
  }
}

// Your actual handler code
async function getWeather(request, context) {
  const response = await WeatherClient.askBadAPIForWeather();
  return {
    statusCode: 200,
    body: JSON.stringify(response),
  };
}

// Your own custom logging function
async function logDuration(duration) {
  console.log(`It took: ${duration}ms to return the weather`);
}

// Your own custom error handler
async function sendError(error) {
  console.error('Handler Error:', error);
  return {
    statusCode: 500,
    body: JSON.stringify({
      error: `${error.message}`,
      message: 'The darn weather API failed me again!',
    }),
  };
}

const TimingMiddleware = timingLogMiddleware(logDuration);
const RecoveryMiddleware = recoveryMiddleware(sendError);
const MyMiddlewareStack = compose(
  TimingMiddleware,
  RecoveryMiddleware,
);

export const lambdaHandler = MyMiddlewareStack(getWeather);