Composable Middleware Builder
raw JSON →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.
Common errors
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(). ↓
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 ↓
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! ↓
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. Warnings
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. ↓
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. ↓
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. ↓
Install
npm install app-builder yarn add app-builder pnpm add app-builder Imports
- compose wrong
const { compose } = require('app-builder')correctimport { compose } from 'app-builder'
Quickstart
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);
});