Centralizing Data Integrity in FeathersJS: A Practical Guide to Validation and Transformation Hooks
Centralize data integrity in FeathersJS with validation and transformation hooks. Learn how to use Joi in a before hook, hash passwords, and keep services clean and consistent across REST and sockets.
03 Jan 2026, 04:12 UTC

Why Hook‑Based Validation Matters
When building a FeathersJS API, you quickly run into the same problem: data coming from clients is often inconsistent or incomplete. Without a single place to enforce rules, you end up scattering checks across routes, adapters, or even the database layer. FeathersJS hooks give you a clean, declarative way to keep the logic in one spot.
What Are Hooks?
Hooks are functions that run automatically at specific points in a service lifecycle: before (before the core method), after (after the method has executed), and error (when an error occurs). They receive the context object, which contains the data, params, and result, and they can modify it or abort the operation.
Typical Use‑Cases
- Validate request payloads with
JoiorYup - Hash passwords or encrypt fields before saving
- Mask sensitive data before sending a response
- Enforce business rules that span multiple services
Hands‑On Example: Validating and Hashing a User Service
Below is a step‑by‑step illustration of adding a before hook that validates the incoming payload with Joi and a second before hook that hashes the password.
// src/services/user/user.hooks.js
const { authenticate } = require('@feathersjs/authentication').hooks;
const { hashPassword } = require('@feathersjs/authentication-local').hooks;
const Joi = require('joi');
const { validateSchema } = require('./validateSchema');
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
role: Joi.string().valid('admin', 'user').default('user')
});
exports.default = {
before: {
create: [
authenticate('jwt'),
validateSchema(userSchema),
hashPassword('password')
]
},
after: {},
error: {}
};
The validateSchema helper is a tiny wrapper that throws a BadRequest error if the payload fails the Joi check.
// src/services/user/validateSchema.js
const { BadRequest } = require('@feathersjs/errors');
module.exports.validateSchema = schema => async context => {
const { data } = context;
const { error } = schema.validate(data, { abortEarly: false });
if (error) {
throw new BadRequest(error.details.map(d => d.message).join(', '));
}
return context;
};
Running the Service
- Install dependencies:
npm install @feathersjs/feathers @feathersjs/authentication @feathersjs/authentication-local joi - Generate a user service with the Feathers CLI:
feathers generate service # Choose "custom" and name it "users" - Replace the generated
user.hooks.jswith the code above. - Start the server:
npm start
Testing the Validation Hook
Send a POST request with curl or Postman to http://localhost:3030/users:
curl -X POST http://localhost:3030/users \
-H "Content-Type: application/json" \
-d '{"email":"invalid","password":"short"}'
You should receive a 400 Bad Request with a message like:
{
"message": ""email" must be a valid email, "password" length must be at least 8 characters"
}
Now try a valid payload:
curl -X POST http://localhost:3030/users \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"StrongPass1"}'
Response:
{
"_id": "60f5c...",
"email": "user@example.com",
"role": "user"
}
Notice the password field is omitted from the response because the hashPassword hook removes it before sending data back.
Hook Ordering and Documentation
Hooks execute in the order they are listed. If you place a transformation hook before validation, the data may no longer match the schema, leading to false negatives. Document the chain explicitly, e.g., with comments or a table:
| Order | Hook | Purpose |
|---|---|---|
| 1 | authenticate('jwt') | Ensure the caller is authorized |
| 2 | validateSchema(userSchema) | Reject invalid payloads early |
| 3 | hashPassword('password') | Securely hash the password before persistence |
Trade‑Offs and Limitations
- Performance Overhead: Each hook runs per request. Heavy logic (e.g., multi‑step encryption) can slow down the API. Offload such tasks to background workers or async hooks if needed.
- Complexity in Large Projects: With many services, maintaining a consistent hook order becomes tedious. Adopt a naming convention or a central hook registry.
- Debugging Difficulty: Errors thrown in a hook propagate to the client. Use
debuglogs or a breakpoint to inspect thecontextwhen troubleshooting. - Version Compatibility: The example uses FeathersJS 5.x. Earlier versions may require different import paths.
Practical Verification Checklist
- Run the service and confirm it starts without errors.
- Send an invalid payload and verify a
400response with a clear message. - Send a valid payload and check that the password is hashed in the database (e.g., by inspecting the stored document).
- Use
feathers-hooks-runneror a unit test framework to run the hook functions in isolation. - Review the
contextobject in a debugger to ensure hooks modify data as expected.
Actionable Takeaway
By moving validation and transformation into FeathersJS hooks, you keep service implementations lean, enforce consistency across REST and socket endpoints, and reduce duplicated logic. Start small: add a validation hook to one service, test thoroughly, then roll it out across the codebase. Remember to document the hook order and keep an eye on performance if you start stacking many hooks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.