Effection Logo
@effectionx/middlewarev0.1.0thefrontside/effectionx
NPM Badge with published version
import { } from "@effectionx/middleware"

Middleware

Type-safe composable middleware for generators and Effection operations

Middleware lets you wrap any function with a chain of interceptors that can inspect arguments, transform return values, or short-circuit execution entirely. The combine() function composes an array of middleware into a single middleware, executing left-to-right.

Because middleware is generic over its return type, generator functions compose through combine() the same way plain functions do. This means you can build middleware pipelines where each layer sets up Effection resources, establishes context, or manages cleanup — and everything below it in the stack inherits that context automatically.

Quick Start

import { combine } from "@effectionx/middleware";
import type { Middleware } from "@effectionx/middleware";
import type { Operation } from "effection";
import { createContext } from "effection";

type Handler = Middleware<[Request], Operation<Response>>;

const DatabaseConnection = createContext<Connection>("database");

// Each middleware is a generator — it's still running while inner layers execute
const withDatabase: Handler = function* (args, next) {
  const conn = yield* connect(process.env.DATABASE_URL);
  yield* DatabaseConnection.set(conn);
  try {
    return yield* next(...args);
  } finally {
    yield* conn.close();
  }
};

const withTransaction: Handler = function* (args, next) {
  const conn = yield* DatabaseConnection.expect();
  const tx = yield* conn.begin();
  try {
    const response = yield* next(...args);
    yield* tx.commit();
    return response;
  } catch (error) {
    yield* tx.rollback();
    throw error;
  }
};

The core handler uses context set up by middleware — but never receives any of it as parameters:

function* handleRequest(request: Request): Operation<Response> {
  const conn = yield* DatabaseConnection.expect();
  const posts = yield* conn.query("SELECT * FROM posts");
  return Response.json(posts);
}

Compose it all together:

const handle = combine([withDatabase, withTransaction]);

function* processRequest(request: Request): Operation<Response> {
  return yield* handle([request], handleRequest);
}

When handleRequest returns (or throws, or is cancelled), the stack unwinds in reverse: withTransaction (commit or rollback) → withDatabase (close connection). Structured concurrency guarantees that no resources leak, even if the request is cancelled mid-flight.

Plain Functions

Middleware also works with plain synchronous functions — there's no Effection dependency. This is useful for argument validation, logging, or result transformation:

import { combine } from "@effectionx/middleware";
import type { Middleware } from "@effectionx/middleware";

const logger: Middleware<[number, number], number> = (args, next) => {
  console.log("adding", args);
  const result = next(...args);
  console.log("result", result);
  return result;
};

const doubler: Middleware<[number, number], number> = (args, next) =>
  next(...args) * 2;

const add = combine([logger, doubler]);

add([3, 4], (a, b) => a + b);
// adding [3, 4]
// result 14
// => 14

API

Middleware<TArgs, TReturn>

The type of a single middleware function. It receives the arguments as a tuple and a next function to delegate to the next middleware (or the core function).

import type { Middleware } from "@effectionx/middleware";

const timer: Middleware<[string], string> = (args, next) => {
  const start = performance.now();
  const result = next(...args);
  console.log(`took ${performance.now() - start}ms`);
  return result;
};

A middleware can:

  • Pass through: call next(...args) and return its result
  • Transform arguments: call next() with different arguments
  • Transform the return value: modify what next() returns
  • Short-circuit: return a value without calling next() at all

combine(middlewares)

Compose an array of middleware into a single middleware. Middlewares execute left-to-right: the first in the array runs outermost.

import { combine } from "@effectionx/middleware";

const composed = combine([logger, validator, retry]);
const result = composed(["hello"], coreFn);
// Execution: logger → validator → retry → coreFn

The returned value is itself a Middleware, so it can be nested inside other combine() calls or passed anywhere a middleware is expected.

API Reference

type Middleware = (args: TArgs, next: (args: TArgs) => TReturn) => TReturn

A function that wraps another function call, with access to the arguments and the ability to delegate to the next link in the chain.

Examples

Example 1
import type { Middleware } from "@effectionx/middleware";

const logger: Middleware<[string], string> = (args, next) => {
  console.log("calling with", args);
  const result = next(...args);
  console.log("returned", result);
  return result;
};

Type Parameters

TArgs extends unknown[]

TReturn

function combine<TArgs extends unknown[], TReturn>(middlewares: Middleware<TArgs, TReturn>[]): Middleware<TArgs, TReturn>

Compose an array of middleware into a single middleware.

Middlewares execute left-to-right: the first middleware in the array is the outermost (runs first), and the last is the innermost (runs just before next).

Examples

Example 1
import { combine } from "@effectionx/middleware";
import type { Middleware } from "@effectionx/middleware";

const logger: Middleware<[string], string> = (args, next) => {
  console.log("calling with", args);
  return next(...args);
};

const stack = combine([logger]);
const result = stack(["hello"], (s) => s.toUpperCase());

Type Parameters

TArgs extends unknown[]

TReturn

Parameters

middlewares: Middleware<TArgs, TReturn>[]

Return Type

Middleware<TArgs, TReturn>