Skip to main content

Middleware

The @daiso-tech/core/middleware module provides a flexible middleware system for intercepting and composing function calls. It enables you to wrap functions with pre-processing and post-processing logic, similar to middleware patterns found in web frameworks like Express.js.

UseFactory configuration

Initial configuration

To begin using middleware, create a middleware application function using the factory:

import { useFactory } from "@daiso-tech/core/middleware";
import { ExecutionContext } from "@daiso-tech/core/execution-context";
import { AlsExecutionContextAdapter } from "@daiso-tech/core/execution-context/als-execution-context-adapter";

// Create a middleware function with a specific execution context
const use = useFactory({
executionContext: new ExecutionContext(new AlsExecutionContextAdapter()),
defaultPriority: 0,
});

Or use the default configuration:

import { useFactory } from "@daiso-tech/core/middleware";

const use = useFactory();

Configure the middleware factory with custom settings:

type UseFactorySettings = {
/**
* The execution context to use for all middleware invocations.
* Defaults to a new ExecutionContext with NoOpExecutionContextAdapter
*/
executionContext?: IExecutionContext;

/**
* Default priority for middleware without an explicit priority.
* Defaults to 0
*/
defaultPriority?: number;
};

const use = useFactory({
executionContext: customContext,
defaultPriority: 50,
});

Middleware basics

Creating a simple middleware

A middleware is a function that receives middleware arguments (containing the original arguments, a next function, and the execution context) and returns the result:

import { type MiddlewareArgs } from "@daiso-tech/core/middleware";
import { type MiddlewareFn } from "@daiso-tech/core/middleware/contracts";

const createLoggingMiddleware = <TParameters extends Array<unknown>, TReturn>(
prefix: string = "LOG",
): MiddlewareFn<TParameters, TReturn> => {
return ({ args, next, context }: MiddlewareArgs<unknown[], unknown>) => {
console.log(`${prefix} - Before invocation with args:`, args);
const result = next(args);
console.log(`${prefix} - After invocation, result:`, result);
return result;
};
};

const loggingMiddleware = createLoggingMiddleware();

Applying middleware to a function

Use the use function to apply one or more middlewares to a function:

const originalFn = (name: string, age: number): string => {
return `${name} is ${age} years old`;
};

const wrappedFn = use(originalFn, loggingMiddleware);

// Call the wrapped function
const result = wrappedFn("Alice", 30);
// Logs: "Before invocation with args: ["Alice", 30]"
// Logs: "After invocation, result: Alice is 30 years old"

Applying multiple middlewares

You can apply multiple middlewares, which are executed in order of their priority:

const createValidationMiddleware = (): MiddlewareFn<
[string, number],
string
> => {
return ({
args,
next,
context,
}: MiddlewareArgs<[string, number], string>) => {
const [name, age] = args;
if (age < 0) throw new Error("Age cannot be negative");
return next(args);
};
};

const createAuthMiddleware = (): MiddlewareFn<[string, number], string> => {
return ({
args,
next,
context,
}: MiddlewareArgs<[string, number], string>) => {
console.log("Checking authorization...");
return next(args);
};
};

const validationMiddleware = createValidationMiddleware();
const authMiddleware = createAuthMiddleware();

const wrappedFn = use(originalFn, [
loggingMiddleware,
validationMiddleware,
authMiddleware,
]);

Middleware types

MiddlewareFn

A function that receives middleware arguments and returns a result:

type MiddlewareFn<TParameters, TReturn> = (
args: MiddlewareArgs<TParameters, TReturn>,
) => TReturn;

IMiddlewareObject

A middleware object with an optional priority property:

class AuthMiddleware implements IMiddlewareObject<[string, number], string> {
constructor(public readonly priority: number = 100) {}

invoke({
args,
next,
context,
}: MiddlewareArgs<[string, number], string>): string {
// Authentication logic
return next(args);
}
}

const authMiddleware = new AuthMiddleware(100);
const wrappedFn = use(originalFn, authMiddleware);

MiddlewareArgs

The argument passed to each middleware:

type MiddlewareArgs<TParameters, TReturn> = {
// Original function arguments
args: TParameters;
// Function to invoke next middleware or original function
next: NextFn<TParameters, TReturn>;
// Execution context for storing request-scoped data
context: IContext;
};

Patterns

Priority-based ordering

Set priority on middleware objects to control execution order (lower numbers execute first):

const createPriorityMiddleware = (
name: string,
priority: number,
): IMiddlewareObject<[string], string> => ({
priority,
invoke: ({ args, next }: MiddlewareArgs<[string], string>): string => {
console.log(`${priority}. ${name}`);
return next(args);
},
});

const authMiddleware = createPriorityMiddleware("Auth", 10);
const validationMiddleware = createPriorityMiddleware("Validation", 20);
const loggingMiddleware = createPriorityMiddleware("Logging", 30);

const wrappedFn = use(
(value: string): string => value.toUpperCase(),
[loggingMiddleware, validationMiddleware, authMiddleware],
);

// Executes in order: Auth -> Validation -> Logging -> Original function

Using execution context

Access and modify the execution context within middleware. For more details about the execution context module, see Execution Context.

import { contextToken } from "@daiso-tech/core/execution-context";
import { Namespace } from "@daiso-tech/core/namespace";
import { type MiddlewareFn } from "@daiso-tech/core/middleware/contracts";

const namespace = new Namespace("myapp");
type UserData = { id: string; name: string };
const userToken = contextToken<UserData>(namespace.create("user").toString());

const createContextAwareMiddleware = (
defaultUser: UserData,
): MiddlewareFn<[string, number], string> => {
return ({
args,
next,
context,
}: MiddlewareArgs<[string, number], string>) => {
const user = context.getOr(userToken, defaultUser);
console.log("Executing as:", user.name);
return next(args);
};
};

const contextAwareMiddleware = createContextAwareMiddleware({
id: "anonymous",
name: "Guest",
});
const wrappedFn = use(originalFn, contextAwareMiddleware);

Async middleware

Middleware can be asynchronous:

const createAsyncValidationMiddleware = (
validator: (args: [string, number]) => Promise<boolean>,
): MiddlewareFn<[string, number], Promise<string>> => {
return async ({
args,
next,
context,
}: MiddlewareArgs<[string, number], Promise<string>>): Promise<string> => {
// Perform async validation
const isValid = await validator(args);
if (!isValid) throw new Error("Validation failed");
return await next(args);
};
};

const asyncValidationMiddleware =
createAsyncValidationMiddleware(validateAsync);
const wrappedFn = use(originalFn, asyncValidationMiddleware);

Short-circuiting middleware

Skip calling next() to bypass subsequent middleware and the original function:

const createCachingMiddleware = <T extends unknown[]>(
cacheStore: Map<string, unknown> = new Map(),
): MiddlewareFn<T, unknown> => {
return ({ args, next, context }: MiddlewareArgs<T, unknown>) => {
const cacheKey: string = JSON.stringify(args);

if (cacheStore.has(cacheKey)) {
console.log("Cache hit!");
return cacheStore.get(cacheKey); // Skip next()
}

const result = next(args);
cacheStore.set(cacheKey, result);
return result;
};
};

const cache = new Map<string, unknown>();
const cachingMiddleware = createCachingMiddleware(cache);

Error handling middleware

Catch and handle errors in middleware:

const createErrorHandlingMiddleware = (
errorHandler?: (error: unknown) => void,
): MiddlewareFn<[string, number], Promise<string>> => {
return async ({
args,
next,
context,
}: MiddlewareArgs<[string, number], Promise<string>>): Promise<string> => {
try {
return await next(args);
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
console.error("Error occurred:", message);
if (errorHandler) errorHandler(error);
throw error;
}
};
};

const errorHandlingMiddleware = createErrorHandlingMiddleware((error) =>
console.log("Error handled gracefully"),
);

Enhancing Methods with enhanceFactory

The enhanceFactory function provides a convenient way to apply middleware to methods of class instances, enabling interception and augmentation of method calls without manually wrapping each function.

Purpose

enhanceFactory is a higher-order factory that, given a use function (created by useFactory), returns an enhance function. This enhance function can be used to dynamically enhance (wrap) a method of an object with one or more middlewares.

Signature

function enhanceFactory(use: Use): Enhance;

Usage Example

import { useFactory, enhanceFactory } from "@daiso-tech/core/middleware";

// Create a middleware application function
const use = useFactory();
const enhance = enhanceFactory(use);

class Greeter {
greet(name: string): string {
return `Hello, ${name}!`;
}
}

const greeter = new Greeter();

// Example middleware that logs calls
function loggingMiddleware<
TParameters extends Array<unknown>,
TReturn,
>(): MiddlewareFn<TParameters, TReturn> {
return ({ args, next }) => {
console.log("Calling greet with:", args);
const result = next(args);
console.log("Result:", result);
return result;
};
}

// Enhance the 'greet' method with middleware
enhance(greeter, "greet", loggingMiddleware());

greeter.greet("Alice");
// Logs:
// Calling greet with: ["Alice"]
// Result: Hello, Alice!

Enhancing Object Literal Methods

You can enhance methods on plain object literals as well:

const obj = {
add(a: number, b: number) {
return a + b;
},
};

enhance(obj, "add", loggingMiddleware());
obj.add(2, 3);
// Logs:
// Calling greet with: [2, 3]
// Result: 5

Enhancing Static Methods

Static methods on classes can also be enhanced:

class MathUtils {
static multiply(a: number, b: number) {
return a * b;
}
}

enhance(MathUtils, "multiply", loggingMiddleware());
MathUtils.multiply(4, 5);
// Logs:
// Calling greet with: [4, 5]
// Result: 20

Enhancing Class Prototype Methods

You can enhance all instances of a class by enhancing its prototype:

class Person {
say(message: string) {
return `Person says: ${message}`;
}
}

enhance(Person.prototype, "say", loggingMiddleware());

const alice = new Person();
alice.say("Hello!");
// Logs:
// Calling greet with: ["Hello!"]
// Result: Person says: Hello!

How it Works

  • The enhance function replaces the specified method on the object with a wrapped version that runs the provided middleware pipeline.
  • If the target property is not a function, a TypeError is thrown.
  • Multiple middlewares can be provided (as an array or single value).

This pattern is useful for adding cross-cutting concerns (logging, validation, authorization, etc.) to class methods in a reusable and declarative way.


UseFactory configuration

Further information

For further information refer to @daiso-tech/core/middleware API docs.