Middleware system for intercepting and modifying service method calls with support for before, after, error, and around hooks.
Register hooks that run for all services in the application.
/**
* Register application level hooks
* @param map - The application hook settings
* @returns The application instance for chaining
*/
hooks(map: ApplicationHookOptions<this>): this;
type ApplicationHookOptions<A> = HookOptions<A, any> | ApplicationHookMap<A>;
interface ApplicationHookMap<A> {
setup?: ApplicationHookFunction<A>[];
teardown?: ApplicationHookFunction<A>[];
}
type ApplicationHookFunction<A> = (
context: ApplicationHookContext<A>,
next: NextFunction
) => Promise<void>;
interface ApplicationHookContext<A = Application> {
app: A;
server: any;
}Usage Examples:
// Application-wide hooks for all services
app.hooks({
before: {
all: [
async (context) => {
console.log(`Calling ${context.method} on ${context.path}`);
}
],
create: [
async (context) => {
context.data.createdAt = new Date();
}
]
},
after: {
all: [
async (context) => {
console.log(`Finished ${context.method} on ${context.path}`);
}
]
}
});
// Application lifecycle hooks
app.hooks({
setup: [
async (context) => {
console.log("Application is setting up");
}
],
teardown: [
async (context) => {
console.log("Application is tearing down");
}
]
});Register hooks for specific services.
/**
* Register service-level hooks
* @param options - Hook configuration
* @returns The service instance for chaining
*/
hooks(options: HookOptions<A, S>): this;
type HookOptions<A, S> = AroundHookMap<A, S> | AroundHookFunction<A, S>[] | HookMap<A, S>;
interface HookMap<A, S> {
around?: AroundHookMap<A, S>;
before?: HookTypeMap<A, S>;
after?: HookTypeMap<A, S>;
error?: HookTypeMap<A, S>;
}
type HookTypeMap<A, S> = SelfOrArray<HookFunction<A, S>> | HookMethodMap<A, S>;
type HookMethodMap<A, S> = {
[L in keyof S]?: SelfOrArray<HookFunction<A, S>>;
} & { all?: SelfOrArray<HookFunction<A, S>> };Usage Examples:
const messageService = app.service("messages");
// Service hooks using different hook types
messageService.hooks({
before: {
all: [
async (context) => {
// Runs before all methods
console.log(`Before ${context.method}`);
}
],
create: [
async (context) => {
// Validate data before creating
if (!context.data.text) {
throw new Error("Text is required");
}
context.data.createdAt = new Date();
}
]
},
after: {
create: [
async (context) => {
// Send notification after creating
console.log("Message created:", context.result);
}
]
},
error: {
all: [
async (context) => {
// Log all errors
console.error(`Error in ${context.method}:`, context.error);
}
]
}
});
// Around hooks for more control
messageService.hooks({
around: {
all: [
async (context, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(`${context.method} took ${duration}ms`);
}
]
}
});Different types of hook functions for various use cases.
/**
* Regular hook function that can modify context
*/
type HookFunction<A = Application, S = Service> = (
this: S,
context: HookContext<A, S>
) => Promise<HookContext<Application, S> | void> | HookContext<Application, S> | void;
/**
* Around hook function that controls execution flow
*/
type AroundHookFunction<A = Application, S = Service> = (
context: HookContext<A, S>,
next: NextFunction
) => Promise<void>;
/**
* Alias for HookFunction
*/
type Hook<A = Application, S = Service> = HookFunction<A, S>;
/**
* Hook types available
*/
type HookType = 'before' | 'after' | 'error' | 'around';The context object passed to all hooks containing request and response data.
interface HookContext<A = Application, S = any> {
/**
* A read only property that contains the Feathers application object
*/
readonly app: A;
/**
* A read only property with the name of the service method
*/
readonly method: string;
/**
* A read only property and contains the service name (or path)
*/
path: string;
/**
* A read only property and contains the service this hook currently runs on
*/
readonly service: S;
/**
* A read only property with the hook type
*/
readonly type: HookType;
/**
* The list of method arguments. Should not be modified
*/
readonly arguments: any[];
/**
* A writeable property containing the data of a create, update and patch service method call
*/
data?: any;
/**
* A writeable property with the error object that was thrown in a failed method call
*/
error?: any;
/**
* A writeable property and the id for a get, remove, update and patch service method call
*/
id?: Id;
/**
* A writeable property that contains the service method parameters
*/
params: Params;
/**
* A writeable property containing the result of the successful service method call
*/
result?: any;
/**
* A writeable property containing a 'safe' version of the data that should be sent to any client
*/
dispatch?: any;
/**
* A writeable, optional property with options specific to HTTP transports
*/
http?: Http;
/**
* The event emitted by this method. Can be set to null to skip event emitting
*/
event: string | null;
}Usage Examples:
// Before hook modifying data
const validateAndEnhance = async (context: HookContext) => {
if (context.method === "create") {
// Validate required fields
if (!context.data.title) {
throw new Error("Title is required");
}
// Add timestamps
context.data.createdAt = new Date();
context.data.updatedAt = new Date();
// Add user info from params
if (context.params.user) {
context.data.userId = context.params.user.id;
}
}
};
// After hook modifying result
const addComputedFields = async (context: HookContext) => {
if (context.result) {
const results = Array.isArray(context.result) ? context.result : [context.result];
results.forEach(item => {
if (item.createdAt) {
item.age = Date.now() - new Date(item.createdAt).getTime();
}
});
}
};
// Error hook for logging
const logErrors = async (context: HookContext) => {
console.error(`Error in ${context.path}.${context.method}:`, {
error: context.error.message,
data: context.data,
params: context.params
});
};
// Around hook for caching
const cacheResults = async (context: HookContext, next: NextFunction) => {
if (context.method === "find") {
const cacheKey = `${context.path}-${JSON.stringify(context.params.query)}`;
const cached = cache.get(cacheKey);
if (cached) {
context.result = cached;
return; // Skip calling next()
}
await next();
if (context.result) {
cache.set(cacheKey, context.result, 60000); // Cache for 1 minute
}
} else {
await next();
}
};Utilities for managing and creating hooks.
/**
* Create a hook context for a service method
* @param service - The service instance
* @param method - The method name
* @param data - Optional context data
* @returns Hook context object
*/
function createContext(service: Service, method: string, data: HookContextData = {}): HookContext;
/**
* Enable hooks functionality on an object
* @param object - The object to enhance with hooks
* @returns Function to register hooks
*/
function enableHooks(object: any): Function;
/**
* Hook manager class for handling hook execution
*/
class FeathersHookManager<A> extends HookManager {
constructor(app: A, method: string);
collectMiddleware(self: any, args: any[]): Middleware[];
initializeContext(self: any, args: any[], context: HookContext): HookContext;
middleware(mw: Middleware[]): this;
}
/**
* Mixin to add hook functionality to services
*/
function hookMixin<A>(
this: A,
service: FeathersService<A>,
path: string,
options: ServiceOptions
): FeathersService<A>;// Method-specific hooks
messageService.hooks({
before: {
find: [hookFunction1, hookFunction2],
create: [validateData, addTimestamp]
}
});
// All methods hooks
messageService.hooks({
before: {
all: [authenticate, authorize]
}
});
// Multiple hook types
messageService.hooks({
before: {
create: [validateData]
},
after: {
create: [sendNotification]
},
error: {
all: [logError]
}
});
// Around hooks only
messageService.hooks([
cacheHook,
timingHook
]);
// Mixed registration
messageService.hooks({
around: {
find: [cacheHook],
all: [timingHook]
},
before: {
create: [validateData]
}
});Authentication Hook:
const authenticate = async (context: HookContext) => {
const token = context.params.headers?.authorization;
if (!token) {
throw new Error("Authentication required");
}
context.params.user = await verifyToken(token);
};Validation Hook:
const validateSchema = (schema: any) => async (context: HookContext) => {
if (context.data) {
const validation = await schema.validate(context.data);
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(", ")}`);
}
}
};Pagination Hook:
const paginate = async (context: HookContext) => {
if (context.method === "find") {
const { $limit = 10, $skip = 0 } = context.params.query || {};
context.params.query = {
...context.params.query,
$limit: Math.min($limit, 100), // Max 100 items
$skip
};
}
};