A lightweight async middleware library with comprehensive TypeScript support.
npm install rowan
Rowan provides a powerful async middleware system with comprehensive TypeScript support. Build sophisticated control-flow patterns with error handling, conditional execution, and post-processing capabilities.
Create a Rowan instance and add middleware using the use
method:
import { Rowan } from 'rowan';
// Create an app instance
const app = new Rowan();
// Add middleware and handlers
app.use(async (ctx) => {
console.log(`Processing: ${ctx.message}`);
});
// Process with context
await app.process({ message: "Hello, World!" });
// Output: Processing: Hello, World!
Rowan supports both modern ESM and legacy CommonJS imports:
// ESM (recommended)
import { Rowan, If, After, Catch } from 'rowan';
// CommonJS
const { Rowan, If, After, Catch } = require('rowan');
Rowan supports three types of processors, each with specific use cases:
Handler functions receive both ctx
and next
parameters. You must explicitly call next()
to continue the middleware chain:
app.use(async (ctx, next) => {
ctx.startTime = Date.now();
await next(); // Continue to next middleware
ctx.duration = Date.now() - ctx.startTime;
console.log(`Request completed in ${ctx.duration}ms`);
});
AutoHandler functions receive only the ctx
parameter. The next middleware is automatically called unless an error is thrown:
app.use(async (ctx) => {
// Automatically calls next() after this function
ctx.data = JSON.parse(ctx.rawData);
ctx.processed = true;
});
Middleware objects implement a process
method with ctx
and next
parameters:
class LoggingMiddleware {
async process(ctx, next) {
console.log('Before processing');
await next();
console.log('After processing');
}
}
app.use(new LoggingMiddleware());
// Or inline object
app.use({
async process(ctx, next) {
await next();
console.log('Request complete');
}
});
Rowan provides powerful helper classes for common middleware patterns:
Execute middleware only when a predicate condition is met:
import { If } from 'rowan';
const app = new Rowan<string>();
app.use(
new If(
async (ctx: string) => ctx.startsWith("admin"),
[
async (ctx) => console.log("Admin access:", ctx)
],
true // terminate if condition is true (don't call next)
)
);
app.use(async (ctx) => {
console.log("Regular access:", ctx);
});
await app.process('admin-user'); // Output: Admin access: admin-user
await app.process('regular-user'); // Output: Regular access: regular-user
Execute middleware after the next middleware completes:
import { After } from 'rowan';
const app = new Rowan();
app.use(new After([
async (ctx) => {
console.log("Response:", ctx.output);
ctx.logged = true;
}
]));
app.use(async (ctx) => {
console.log("Processing request...");
ctx.output = `Processed: ${ctx.input}`;
});
await app.process({ input: "hello" });
// Output:
// Processing request...
// Response: Processed: hello
Execute middleware after next() completes, but only if a condition is met:
import { AfterIf } from 'rowan';
const app = new Rowan();
app.use(new AfterIf(
async (ctx) => ctx.valid === true,
[
async (ctx) => {
console.log("Valid result:", ctx.result);
}
]
));
app.use(async (ctx) => {
console.log("Validating...");
if (ctx.input?.length > 5) {
ctx.valid = true;
ctx.result = `Valid: ${ctx.input}`;
}
});
await app.process({ input: "hello" }); // Only "Validating..."
await app.process({ input: "hello world" }); // "Validating..." then "Valid result: Valid: hello world"
Wrap middleware execution with comprehensive error handling:
import { Catch } from 'rowan';
const app = new Rowan();
app.use(
new Catch(
async (error, ctx) => {
console.log("Error caught:", error.message);
ctx.error = true;
ctx.errorMessage = error.message;
// Don't re-throw to handle gracefully
},
async (ctx) => {
if (!ctx.input) {
throw new Error("Input is required");
}
ctx.processed = true;
}
)
);
await app.process({ input: "hello" }); // Normal processing
await app.process({}); // Error caught: Input is required
Execute a sequence of middleware with automatic chaining:
import { Rowan } from 'rowan';
const middlewares = [
{
async process(ctx, next) {
console.log("First middleware");
await next();
}
},
{
async process(ctx, next) {
console.log("Second middleware");
await next();
}
}
];
await Rowan.process(middlewares, { message: "hello" }, async () => {
console.log("Final step");
});
// Output:
// First middleware
// Second middleware
// Final step
Build a meta hierarchy from middleware with metadata:
import { Rowan } from 'rowan';
const app = new Rowan([], { name: "App" });
const subRouter = new Rowan();
subRouter.meta = { name: "SubRouter" };
subRouter.use(async (ctx, next) => {
await next();
}, { name: "Handler1" });
subRouter.use({
meta: { name: "Handler2" },
async process(ctx, next) {
await next();
}
});
app.use(subRouter);
const hierarchy = Rowan.hierarchy(app);
console.log(JSON.stringify(hierarchy, null, 2));
// Output:
// {
// "meta": { "name": "App" },
// "children": [
// {
// "meta": { "name": "SubRouter" },
// "children": [
// { "meta": { "name": "Handler1" } },
// { "meta": { "name": "Handler2" } }
// ]
// }
// ]
// }
Convert handlers to middleware objects with metadata:
import { Rowan } from 'rowan';
const handler = async (ctx) => {
ctx.processed = true;
};
const middleware = Rowan.convertToMiddleware(handler, { name: "ProcessHandler" });
console.log(middleware);
// Output:
// {
// meta: { name: "ProcessHandler" },
// process: [Function]
// }
import { isMiddleware, isAutoHandler } from 'rowan';
const handler = async (ctx) => {};
const middleware = { async process(ctx, next) {} };
console.log(isAutoHandler(handler)); // true
console.log(isMiddleware(middleware)); // true
console.log(isMiddleware(handler)); // false
# Install dependencies
npm install
# Run tests
npm test
# Run tests with coverage
npm run test:cover
# Lint code
npm run lint
# Build for production (ESM + CJS)
npm run build
# Clean build artifacts
npm run clean
rowan/
├── src/ # TypeScript source files
│ ├── rowan.ts # Core Rowan class and types
│ ├── if.ts # If conditional middleware
│ ├── after.ts # After post-processing middleware
│ ├── after-if.ts # AfterIf conditional post-processing
│ ├── catch.ts # Catch error handling middleware
│ └── index.ts # Main exports
├── test/ # Test files (Mocha + Chai)
├── dist/ # Built output
│ ├── esm/ # ES Module build
│ └── cjs/ # CommonJS build
└── coverage/ # Coverage reports
- Node.js >= 18.0.0
- TypeScript >= 5.0.0 (for development)
MIT © Meirion Hughes
"Rowan" Icon courtesy of The Noun Project, by ludmil, under CC 3.0