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.

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();

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 MiddlewareFn,
type MiddlewareArgs,
} from "@daiso-tech/core/middleware";

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";

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"),
);

UseFactory configuration

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,
});

Further information

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