npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

middleware-kit

v1.0.0

Published

A small, universal engine for composing async functions using the onion (middleware) pattern — not tied to HTTP.

Readme

middleware-kit

A tiny, runtime-agnostic middleware composition engine based on the onion execution model (before → await next() → after).

Compose reusable async pipelines for HTTP servers, RPC, WebSockets, background jobs, CLIs, or any workflow built around next() — not another HTTP framework, a composition primitive you build on top of.

bun add middleware-kit

Why

koa-compose already exists as a ~20-line standalone package. middleware-kit keeps that same minimal core, but adds tooling that stays completely opt-in:

  • Runtime-agnostic cancellation
  • Static pipeline introspection (explain)
  • Runtime tracing (traceable)
  • First-class TypeScript generics
  • Structured runtime errors

The core dispatch loop (compose()) is deliberately blind to logging, retries, priorities, metrics, or any other cross-cutting concern. Those are implemented as decorators that wrap middleware before it reaches compose(), keeping the execution engine small, predictable, and easy to audit.

Features

| Feature | Supported | |---|:---:| | Runtime-agnostic | ✅ | | TypeScript-first, generic context | ✅ | | Onion execution model | ✅ | | Static pipeline introspection | ✅ | | Runtime tracing | ✅ | | Structured errors | ✅ | | Cancellation (AbortSignal-compatible) | ✅ | | HTTP-independent | ✅ |


Quick start

import { compose } from "middleware-kit";

const pipeline = compose([
  async (ctx, next) => {
    console.log("before A");
    await next();
    console.log("after A");
  },
  async (ctx, next) => {
    console.log("before B");
    await next();
    console.log("after B");
  },
]);

await pipeline({});
// before A
// before B
// after B
// after A

See examples/basic-usage.ts for a fuller example combining compose, explain, traceable, error handling, and cancellation in one pipeline.


Typed context

compose() is generic over the context shape, so every middleware in the array is checked against the same type:

interface AuthContext {
  user?: { id: string };
}

const pipeline = compose<AuthContext>([authMiddleware, permissionsMiddleware]);

Named middlewares

Plain functions work fine, but naming a middleware makes it show up in explain() output, trace events, and error messages instead of an autogenerated anonymous_N:

const auth = {
  name: "auth",
  handler: async (ctx, next) => {
    ctx.user = await authenticate(ctx);
    await next();
  },
  meta: { critical: true }, // free-form, your own tooling can read this
};

compose([auth, cacheMiddleware, controller]);

Error handling

Outer middlewares can catch errors thrown by inner ones, the same way try/catch works around nested synchronous function calls:

compose([
  async (ctx, next) => {
    try {
      await next();
    } catch (err) {
      console.error("caught:", err);
    }
  },
  async () => {
    throw new Error("inner failure");
  },
]);

Errors propagate whether they're thrown synchronously or via a rejected promise — both are normalized the same way. You can also attach a global hook that runs before an error propagates, useful for centralized error reporting:

await pipeline(ctx, {
  onError: (err, layer) => report(err, layer.name),
});

Calling next() more than once in the same middleware throws a MultipleNextCallError rather than silently re-running the downstream pipeline.


Cancellation

compose() accepts any object implementing the lightweight CancellationSignal interface. A real AbortController works out of the box:

const controller = new AbortController();

const pipeline = compose([longRunningMiddleware]);

await pipeline({}, { signal: controller.signal });
// rejects with AbortError if aborted before or during execution

Custom implementations are also supported — useful in environments without a native AbortSignal:

await pipeline(ctx, {
  signal: { aborted: false },
});

Because cancellation is typed against this minimal interface instead of the DOM's AbortSignal directly, middleware-kit works the same way across browsers, Node.js, Bun, Deno, workers, and custom JavaScript runtimes.


explain() — static introspection

Describes a pipeline's shape without executing anything — useful for debug tooling, CI checks on pipeline order, or generating docs:

import { explain } from "middleware-kit";

const middlewares = [
  { name: "logger", handler: loggerMw },
  { name: "auth", handler: authMw },
  { name: "controller", handler: controllerMw },
];

console.log(explain(middlewares).asText);
// 1. logger
// 2. auth
// 3. controller

traceable() — runtime tracing

Wraps middlewares with enter/exit/error instrumentation before composition, so compose() itself never changes and there is zero overhead when tracing isn't used:

import { compose, traceable } from "middleware-kit";

const events = [];
const pipeline = compose(
  traceable(middlewares, { onEvent: (e) => events.push(e) })
);

await pipeline({});
console.table(events);

Events carry a type ("enter" | "exit" | "error"), the originating layer, and a durationMs on exit/error. Because traceable() returns a plain middleware array, it composes with other decorators you might add later (a future withRetry(), for instance) and works with any compose-compatible engine — not just this one.


Design principles

  • The core stays small. compose() is deliberately blind to priorities, retries, and logging — those are decorators applied to the middleware array before it reaches compose().
  • Not HTTP-specific. Context is a generic TContext; use it for HTTP, WebSockets, workers, jobs, RPC, message queues, CLIs, or state machines.
  • Zero-cost when unused. explain and traceable are opt-in layers with no effect on the hot path if you never call them.
  • Runtime-agnostic. No dependency on browser APIs or any specific runtime — runs anywhere modern JavaScript runs.

API reference

| Export | Description | |---|---| | compose(middlewares) | Builds an executable pipeline from an array of middlewares. | | explain(middlewares) | Static, non-executing description of a pipeline's shape. | | traceable(middlewares, options) | Wraps middlewares with enter/exit/error event instrumentation. | | normalize(middlewares) | Converts a mixed array of functions/NamedMiddleware into a uniform NamedMiddleware[]. | | CancellationSignal | Minimal, runtime-independent cancellation interface accepted by compose(). | | AbortError | Thrown when a pipeline is cancelled via a CancellationSignal. | | MultipleNextCallError | Thrown when next() is called more than once in the same layer. |

Full type-level documentation (JSDoc/TSDoc) lives alongside each export in src/types.ts, src/compose.ts, src/explain.ts, and src/trace.ts.


Project structure

middleware-kit/
├── src/
│   ├── compose.ts   # core dispatch engine
│   ├── explain.ts   # static pipeline introspection
│   ├── trace.ts     # runtime tracing decorator
│   ├── types.ts     # shared types and errors
│   └── index.ts     # public exports
├── examples/
│   └── basic-usage.ts
└── test/
    ├── compose.test.ts
    └── trace.test.ts

Development

bun install       # install dependencies
bun run test      # run the test suite (vitest)
bun run typecheck # type-check without emitting
bun run check     # lint + format check (biome)
bun run build     # produce dist/ (esm + cjs + .d.ts) via tsup

License

MIT