Ordering FeathersJS Hooks So Validation and Auth Don't Fight Your Database
A practical look at using FeathersJS hooks to centralize validation and business logic, with a worked example, ordering pitfalls, and how to verify hook execution.
12 Oct 2026, 11:21 UTC

Putting validation and user-context logic directly inside every FeathersJS service method works until it doesn't. You end up copying the same if (!params.user) check into create, patch, and remove across a dozen services. Worse, a small refactor can leave an authentication check running after you've already mutated the payload. FeathersJS hooks exist to solve exactly this, but they only help if you treat them as a deliberate pipeline rather than a dumping ground.
The useful takeaway: hooks are middleware for your service layer. When scoped and ordered correctly, they keep database adapters dumb and business rules in one place. When scoped carelessly, they turn a simple request into a stack-trace scavenger hunt.
How a request moves through hooks
Every FeathersJS service method call—whether it arrives via REST, Socket.io, or an internal app.service('messages').create()—passes through a hook chain. Before hooks run first and can mutate context.data, context.params, or throw an error to stop the request. After hooks run once the service method returns and can inspect or transform context.result. The context object is the contract: it carries the service, the incoming data, the params (including authentication info), and eventually the result.
Hooks are plain functions. They can be synchronous or return a promise, which means you can await a database lookup or an external API call before the service method executes.
Version note: Examples below assume FeathersJS v5 (Dove) with CommonJS modules. If you use TypeScript or an earlier major version, import paths and file extensions may differ; confirm against your installed version's documentation.
Worked example: a messages service that validates and stamps ownership
Imagine a messages service. You need three things before a message hits the database: the user must be authenticated, the message body must be non-empty, and the record must carry the authenticated user's ID. Doing this inside the adapter couples your service to auth logic; doing it in a controller duplicates it. A hook chain handles it once.
Create a custom hook in src/hooks/process-message.js:
// src/hooks/process-message.js
module.exports.validateMessage = async (context) => {
const { data } = context;
if (!data || typeof data.text !== 'string' || data.text.trim() === '') {
throw new Error('Message text is required');
}
return context;
};
module.exports.setCreatedBy = async (context) => {
const { data, params } = context;
// params.user is populated by the authentication hook
if (params.user) {
data.createdBy = params.user._id;
}
return context;
};
Apply the chain in the service configuration file, src/services/messages/messages.js:
const { authenticate } = require('@feathersjs/authentication');
const { validateMessage, setCreatedBy } = require('../../hooks/process-message');
exports.before = {
create: [
authenticate('jwt'),
validateMessage,
setCreatedBy
]
};
The order matters. authenticate('jwt') runs first so params.user exists by the time setCreatedBy reads it. validateMessage runs before any database write, so malformed payloads never reach the adapter. If you reversed validateMessage and setCreatedBy, you would still reject bad data, but you would be reading params.user in a hook that runs before authentication—a silent bug that only appears when you test with a valid token.
Global hooks vs. service hooks: where to draw the line
FeathersJS lets you register hooks globally in app.hooks(), per service in the service file, or per method inside the before/after objects. The temptation is to push everything global. Resist it.
Global hooks are best for truly universal concerns: request logging, generic error formatting, or a params sanitizer that strips unknown query fields from every service. Service-level hooks belong to domain logic—ownership stamps, field-level validation, or pagination defaults specific to one resource. Method-level hooks (like before.create above) are for rules that apply to exactly one operation.
The trade-off is debuggability. A chain of five global hooks plus three service hooks can make it hard to answer "why did my data object change?" without stepping through every function. Keep each hook single-purpose, name it after what it does, and avoid mutating context.data unless validation has already run.
Verifying the hook chain actually executes
Because hooks are just functions, the fastest verification is a real request against the endpoint. With your app running locally, send an authenticated POST from your terminal:
curl -X POST http://localhost:3030/messages \
-H "Authorization: Bearer <your-jwt-token>" \
-H "Content-Type: application/json" \
-d '{"text":"Hello from a hook test"}'
You need the Feathers app running and a valid JWT for a user that exists in your database. If the chain works, the response status should indicate success and the returned record should include the createdBy field set by setCreatedBy. If createdBy is missing, either authenticate('jwt') did not populate params.user or the hook ordering is wrong. If you receive a validation error, validateMessage correctly intercepted the payload.
One limitation to plan for: deep hook chains do not show up in a single stack trace. If a request fails, add temporary console.log(context.data) statements inside each hook during development, or use FeathersJS's built-in debug logging to see the hook sequence. Remove them before deploying.
Actionable next steps
- Audit your services for duplicated validation or auth checks and move them into named hooks.
- Order every
beforechain with authentication first, data validation second, and transformation last. - Keep global hooks to cross-cutting infrastructure only; push domain rules down to the service or method level.
- Write one integration test per hook chain that sends a real request and asserts on the database result, not just the service method return value.
Hooks will not make your architecture clean by themselves, but they give you a single, inspectable place to put the rules that every service shares.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.