Composable Middleware Builder

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

app-builder is a JavaScript library for creating robust and composable asynchronous middleware pipelines, following the 'onion' model commonly seen in frameworks like Koa. It leverages Promises to enable clear and sequential execution of middleware functions, each capable of performing operations before and after subsequent middleware in the stack. The current stable version is 7.0.4. While a specific release cadence isn't explicitly stated, the active GitHub repository suggests ongoing maintenance. Its key differentiator lies in its simplicity and functional approach to middleware composition, providing a lightweight alternative to larger frameworks for building request-response flows or general data processing pipelines. It ships with TypeScript types, ensuring a better developer experience in TypeScript projects.

error UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing an error inside an async function without a catch block, or by rejecting a promise which was not handled with .catch().
cause An asynchronous middleware function threw an error, and no upstream middleware in the pipeline or the final `.catch()` handler on the composed app caught it.
fix
Wrap await next() calls in a try/catch block within your middleware, or ensure an error-handling middleware is correctly positioned at the beginning of your pipeline to catch errors from subsequent middleware.
error TypeError: next is not a function
cause A function passed into `compose` did not correctly receive `(ctx, next)` arguments, or `next()` was called outside its scope or with incorrect context.
fix
Ensure all functions passed to compose adhere to the async (ctx, next) signature, even if next is not used. Verify the order of arguments.
error My outer middleware log appeared before my inner middleware log, but it should have been after!
cause The `await` keyword was omitted when calling `next()` in an `async` middleware function, causing the outer middleware to proceed before the inner middleware completed.
fix
Always use await next() in async middleware functions to ensure the 'onion' model's control flow is maintained and execution returns to the current middleware after the inner stack is complete.
gotcha Forgetting to `await next()` within an asynchronous middleware function will cause the outer middleware to complete its 'after' logic immediately, before inner middleware has finished executing. This breaks the intended 'onion' model of execution.
fix Always ensure `await next()` is used when calling the next middleware in an `async` function if you expect control to return to the current middleware after the inner stack has completed.
gotcha Errors thrown by asynchronous middleware functions will result in unhandled promise rejections if not explicitly caught by a `try/catch` block within the middleware itself or by a dedicated error-handling middleware placed higher in the composition.
fix Implement an error-handling middleware that wraps `await next()` in a `try/catch` block, or ensure individual middleware functions handle their own potential errors.
gotcha While flexible, extensive mutation of the context object across many middleware functions without clear contracts can lead to difficult-to-debug side effects and data flow issues, especially in complex pipelines.
fix Establish clear conventions for context properties and their expected types/values at each stage of the pipeline. Consider using immutable data patterns or a more structured context object where applicable.
npm install app-builder
yarn add app-builder
pnpm add app-builder

Demonstrates how to compose asynchronous middleware functions into a pipeline, including logging, core logic, and robust error handling, showcasing both successful and failing scenarios.

import { compose } from 'app-builder';

// Define a simple logger middleware that logs entry and exit
const loggerMiddleware = async (ctx, next) => {
  console.log(`Entering middleware: ${ctx.name}`);
  await next(); // This is crucial to pass control to the next middleware
  console.log(`Exiting middleware: ${ctx.name}`);
};

// Define a core logic middleware that modifies the context
const coreLogicMiddleware = async (ctx, next) => {
  ctx.data = 'Processed data';
  console.log(`Core logic executed, data: ${ctx.data}`);
  await next();
};

// Define an error handling middleware to gracefully catch exceptions
const errorHandlingMiddleware = async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    console.error(`An error occurred: ${error.message}`);
    ctx.error = error.message; // Store error in context
  }
};

// Compose the middleware pipeline
const app = compose(
  loggerMiddleware, // First, logs entry
  errorHandlingMiddleware, // Catches errors from subsequent middleware
  coreLogicMiddleware, // Performs core data processing
  async (ctx, next) => {
    // This is an inline middleware demonstrating further processing
    ctx.status = 'completed';
    console.log(`Final status set to: ${ctx.status}`);
    await next(); // Even if it's the last, always call next()
  }
);

// Define an initial context object
const initialContext = { name: 'MyPipeline', data: null, status: null, error: null };

// Execute the pipeline
app(initialContext)
  .then(() => {
    console.log('Pipeline finished. Final context:', initialContext);
  })
  .catch(err => {
    console.error('Unhandled pipeline error (should not happen with errorHandlingMiddleware):', err);
  });

// Example with an intentional error to show error handling
const failingApp = compose(
  errorHandlingMiddleware, // This will catch the error
  async (ctx, next) => {
    console.log('Failing middleware attempting to run...');
    throw new Error('Something went wrong in the pipeline!');
  }
);

const failingContext = { name: 'FailingPipeline', error: null };
failingApp(failingContext)
  .then(() => {
    console.log('Failing pipeline finished. Final context:', failingContext);
  })
  .catch(err => {
    console.error('Unhandled error in failing pipeline execution (should not happen):', err);
  });