When to Use FeathersJS Service Hooks vs Middleware or Custom Methods
A compact decision guide for FeathersJS: when service hooks beat Express middleware or overriding methods, with trade‑offs and a concrete before‑create validation hook example.
02 Aug 2026, 07:25 UTC

The decision
You need input validation, permission checks, logging or data transformation that applies to a Feathers service without duplicating logic in every method. The constraint is to keep cross‑cutting concerns declarative, reusable, and testable while preserving Feathers’ context and real‑time behavior.
Use Feathers service hooks for cross‑cutting concerns that belong to the service lifecycle. Use Express middleware for transport‑level concerns and override service methods only when you need full control over a single operation.
Options at a glance
| Option | When it fits | Context access | Reuse |
|---|---|---|---|
| Feathers hooks | Validation, authorization, normalization, audit logging per service/method | Full hook context: type, params, app, id, data, result | High. Register once per service or share as a plugin |
| Express middleware | Request parsing, CORS, rate limiting, authentication at transport layer | Express req/res only. No Feathers service context | Medium. Runs for all routes before Feathers routing |
| Override service methods | Custom business flow that replaces default create/get/update behavior | Full control inside method | Low. Logic is coupled to the service class |
Trade‑offs
Hooks run inside the Feathers service lifecycle and are ordered, async‑aware, and service‑specific. They receive a rich context object and integrate with FeathersError handling. Errors thrown as feathers.errors.* map to consistent HTTP and Socket.io responses.
Express middleware is lighter and generic, but it executes before Feathers routing. It cannot easily inspect or modify hook context, and it can interfere with Feathers real‑time channels if it short‑circuits the request.
Overriding methods gives full control but bypasses the hook chain. It couples logic to the implementation, reduces pluggability, and makes unit testing harder because concerns are mixed with service logic.
Limitations to keep in mind: hooks must return a promise or call next() when asynchronous work is performed; forgetting to do so can let the service proceed before validation completes. Embedding complex business logic in many hooks obscures flow; extract heavyweight operations to a custom service, plugin, or module and keep hooks focused on lightweight concerns.
Concrete validation hook
Example service: /messages with required fields text and userId. A before create hook validates input and throws BadRequest if missing.
// src/hooks/require-fields.js
const { BadRequest } = require('@feathersjs/errors');
module.exports = function requireFields(fields) {
return async context => {
const data = context.data || {};
const missing = fields.filter(f => data[f] === undefined || data[f] === '');
if (missing.length) {
throw new BadRequest(`Missing required fields: ${missing.join(', ')}`);
}
return context;
};
};
// src/services/messages/messages.service.js
const { Service } = require('feathers-memory');
const requireFields = require('../../hooks/require-fields');
class MessagesService extends Service {}
module.exports = function (app) {
const options = { name: 'messages' };
app.use('/messages', new MessagesService(options));
const service = app.service('messages');
service.hooks({
before: {
create: [requireFields(['text', 'userId'])]
}
});
};
How to check the result
- Send a
POSTto/messageswith a payload missingtext. Expected check: response is HTTP 400 with aBadRequesterror message indicating the missing field. - Send a
POSTto/messageswith bothtextanduserIdpresent. Expected check: service proceeds, returns the created record, and any subsequent after hooks execute.
Monitor logs for hook execution order and verify that context.result is populated only after successful validation. If validation is skipped, review hook registration and ensure the hook returns context or a promise.