FeathersJS Service Hooks: Validation, Transformation, and Authentication in One Pipeline
FeathersJS hooks let you centralise validation, transformation, and authentication. Learn how to stack hooks correctly, avoid pitfalls, and keep your API secure and performant.
05 Nov 2025, 13:07 UTC

Problem & Takeaway
When building a FeathersJS API, you often need to enforce data contracts, hide sensitive fields, and secure endpoints. Writing custom code in every service for these concerns quickly becomes duplicated and error‑prone. FeathersJS hooks give you a declarative, composable layer that runs before or after service methods. The key takeaway: hooks let you centralise validation, transformation, and authentication without touching core service logic, but you must manage their order carefully to avoid security gaps or performance regressions.
Hook Basics
Hooks are functions that receive a context object representing the current request. They can be attached to specific service methods (e.g., create, update) or to all methods. Feathers executes hooks in three phases:
- before – runs before the service method.
- after – runs after the service method returns data.
- error – runs if the service method throws.
Each phase can contain multiple hooks, executed in the order they are defined. The context object is mutable; hooks can modify context.data or context.result to transform payloads.
Practical Hook Stack Example
- Validation (before) – ensure payload matches a Joi schema.
- Authentication (before) – verify JWT and attach user info.
- Transformation (after) – strip
passwordfrom the response. - Logging (after) – record request metadata.
Below is a concrete configuration for a users service that demonstrates these hooks.
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const { Service } = require('feathers-memory');
const { hooks } = require('@feathersjs/hooks');
const { authenticate } = require('@feathersjs/authentication').hooks;
const Joi = require('joi');
const app = express(feathers());
app.use(express.json());
// 1. Authentication setup
const jwtAuth = require('@feathersjs/authentication-jwt');
app.configure(require('@feathersjs/authentication')({
secret: 'super-secret',
strategies: ['jwt'],
}));
app.configure(jwtAuth());
// 2. Joi validation hook
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
});
const validateUser = {
before: {
create: [
async context => {
const { error } = userSchema.validate(context.data);
if (error) {
throw new Error(`Validation error: ${error.message}`);
}
return context;
},
],
},
};
// 3. Transformation hook
const maskPassword = {
after: {
find: [
async context => {
context.result.data = context.result.data.map(u => {
const { password, ...rest } = u;
return rest;
});
return context;
},
],
get: [
async context => {
const { password, ...rest } = context.result;
context.result = rest;
return context;
},
],
},
};
// 4. Service definition
app.use('/users', new Service({
paginate: { default: 10, max: 25 },
}));
app.service('users').hooks({
before: {
all: [authenticate('jwt')], // ensures token present
create: [hooks.disable('patch')], // example of disabling a method
},
after: [maskPassword],
error: [],
});
// 5. Validation hook
app.service('users').hooks(validateUser);
module.exports = app;
Verifying the Hook Stack
- Validation test: POST /users with
{email: "bad", password: "123"}should return400. - Authentication test: GET /users without
Authorizationheader should return401. - Transformation test: Successful GET /users should not include
passwordin any user object.
Run these tests with curl or Postman. For example:
curl -X POST http://localhost:3030/users \
-H "Content-Type: application/json" \
-d '{"email":"bad","password":"123"}'
# Expect 400
curl -X GET http://localhost:3030/users \
-H "Authorization: Bearer "
# Expect 401
Hook Order Pitfalls
Hook execution order is strict. A common mistake is placing a transformation hook before authentication. If the maskPassword hook runs before authenticate, an unauthenticated user could receive a sanitized payload that still reveals the existence of a user record.
| Phase | Hook | Order |
|---|---|---|
| before | authenticate | 1 |
| before | validate | 2 |
| after | maskPassword | 1 |
Always list authentication hooks before any data‑modifying hooks that could expose sanitized data to unauthenticated clients.
Performance Considerations
Hooks are synchronous by default but can perform async operations. Heavy validation (e.g., complex regex, database lookups) or introspection (e.g., logging every request field) will add latency. Measure latency with and without hooks using a simple benchmark:
time curl -X POST http://localhost:3030/users -d '{"email":"[contact removed]","password":"secret123"}'
Typical overhead for a Joi validation hook is < 5 ms on a modern CPU. If you notice higher latency, consider moving non‑critical validation to the client or using a lighter schema library.
Recursive Hook Risks
Invoking the same service inside a hook (e.g., await context.app.service('users').create()) can trigger the hook stack again, potentially creating an infinite loop. Guard recursive calls with a flag in context or use context.app.service('users').create({ ... }, { hooks: false }) to skip hooks for internal calls.
Trade‑Off: Centralised vs. Localised Logic
Centralising validation and transformation via hooks reduces duplication but can make debugging harder: a bug in a hook may affect many services. Conversely, moving logic into the service directly keeps the flow explicit but duplicates code. A balanced approach is to keep simple, reusable hooks (e.g., schema validation) in a shared module and service‑specific logic inside the service.
Actionable Checklist
- Define Joi/Yup schemas for every service that receives user input.
- Attach validation hooks in the
beforephase, before authentication. - Place authentication hooks first in the
beforearray. - Use
afterhooks for data masking or enrichment. - Profile request latency; if >10 ms per hook, consider optimisation.
- Guard recursive service calls to avoid loops.
- Document hook order in README to aid future developers.
Conclusion
FeathersJS hooks are a powerful, declarative way to enforce validation, hide sensitive data, and secure services. By carefully ordering hooks and monitoring performance, you can keep your API clean, secure, and maintainable. Start by moving your existing validation and masking logic into shared hooks, then iterate on the order based on real traffic patterns.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.