Centralize Service Logic with FeathersJS Hooks
Use FeathersJS service hooks to inject createdBy, validate input, and stop invalid writes without repeating code in every service.
04 Feb 2026, 13:36 UTC

The problem: repeating the same setup in every service
You are building an API with FeathersJS. Several services—messages, tasks, and documents—all need to stamp new records with the ID of the authenticated user who created them. Without a shared pattern, you end up copying the same `createdBy` assignment into every `create` method. One day someone forgets, and a record saves without an owner.
The useful takeaway is that FeathersJS service hooks let you centralize that logic once. A hook intercepts CRUD calls—`create`, `get`, `update`, `patch`, `remove`, and `find`—before or after the service runs, so you can validate, enrich, or block requests without touching the service implementation itself.
Worked example: auto-populate createdBy on create
Suppose you have a `messages` service in `src/services/messages/messages.service.js`. Instead of editing the service class, create a hook file and register it in the service's hooks export.
// src/hooks/set-created-by.js
module.exports = function setCreatedBy() {
return async function (context) {
if (context.method !== 'create') {
return context;
}
const { user, data } = context.params;
if (!user) {
throw new Error('Authentication required to create a message');
}
context.data = {
...data,
createdBy: user.id,
createdAt: new Date().toISOString(),
};
return context;
};
};
Run this from your project root in a terminal. No special permissions are required beyond normal file access. Replace `messages` with your actual service name when registering the hook.
// src/services/messages/messages.service.js
const { setCreatedBy } = require('../../hooks/set-created-by');
module.exports = function (app) {
const service = app.use('messages', {
// service definition
});
service.hooks({
before: {
create: [setCreatedBy()],
},
});
};
When a POST request hits `/messages`, the `setCreatedBy` hook runs first. If `context.params.user` is missing, it throws and Feathers stops the chain before the database write happens. If the user exists, the hook adds `createdBy` and `createdAt`, then the service proceeds with normalization.
What happens if a hook throws
Feathers treats a thrown error as a stop signal. Later hooks in the same phase do not run, and the service method does not execute. This protects against invalid writes, but it also means a hook that mutates external state before throwing can leave partial changes behind. Keep hooks idempotent where possible, and avoid side effects outside the hook context.
Verify the hook is running
- Open `src/services/messages/messages.service.js` and confirm `service.hooks` contains a `before.create` array with your hook function.
- Add a temporary `console.log('setCreatedBy ran', context.data)` inside the hook, restart the server, and send a test request:
curl -X POST http://localhost:3030/messages \
-H "Content-Type: application/json" \
-d '{"text":"hello"}'
Expected check: the server log shows the hook ran, and the stored record includes `createdBy`. Risk: if authentication middleware has not populated `context.params.user`, the hook throws an error that Feathers converts to an HTTP failure unless you handle it.
Trade-off: visibility versus convenience
Hooks keep service classes small, but long hook chains across many services can make execution order hard to trace. If you stack authentication, validation, logging, and business logic hooks on one method, a bug in one hook can silently block the next. Document the order in a comment or README, and keep each hook focused on a single responsibility.
Closing
Start with one cross-cutting concern, such as setting audit fields or enforcing ownership, and implement it as a `before` hook. Verify with a curl request and server logs before adding more hooks to the chain.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.