Keep FeathersJS Services Thin with Hooks
Extract validation, authorization, and response shaping into FeathersJS hooks so your service methods stay focused on business logic.
01 Dec 2025, 05:28 UTC

The problem: service methods absorb every concern
In a growing FeathersJS application, service methods tend to collect responsibilities that do not belong to them. A single create method might validate the incoming payload, check whether the authenticated user owns the target resource, normalize dates, write to the database, and then strip private fields before sending the result to the client. Because FeathersJS services work across REST, Socket.io, and other transports, duplicating that logic in every method quickly makes the service layer hard to test and harder to change.
The useful takeaway is that hooks let you move those cross-cutting concerns out of the service method and into small, reusable functions. The service method stays focused on data access, while hooks handle validation, authorization, and response shaping consistently for every transport.
How the hook pipeline runs
A FeathersJS service exposes standard methods such as find, get, create, update, patch, and remove. Hooks are plain functions that wrap those methods. They run in three stages:
- before: runs before the service method. Use it to validate input, normalize
context.data, or check permissions fromcontext.params. - after: runs after the method succeeds. Use it to transform
context.result, such as removing sensitive fields. - error: runs when a before hook or the service method throws. Use it to log failures or reshape error responses.
Each hook receives a context object containing method, params, data, and result, depending on the stage. Before hooks run in the order they are registered; after hooks run in reverse order, so outer layers can wrap inner layers predictably.
Registration style and the exact fields on context.params differ between Feathers major versions. Confirm the API for your installed @feathersjs/feathers version before adapting the example below.
Worked example: ownership check and field stripping
Imagine a messages service. You want to ensure that only the message creator can patch it, and you never want the internalNote field to reach the client. Instead of putting both checks inside patch, register two before hooks and one after hook.
// hooks/message-hooks.js
// Placeholder: adjust import paths and registration style for your Feathers version.
function validatePatchInput(context) {
if (!context.data || typeof context.data.text !== 'string') {
const error = new Error('text is required');
error.code = 400;
throw error;
}
return context;
}
async function checkMessageOwnership(context) {
const { params, id } = context;
const message = await context.service.get(id, params);
if (message.userId !== params.user?.id) {
const error = new Error('not authorized');
error.code = 403;
throw error;
}
return context;
}
function stripInternalFields(context) {
if (context.result && context.result.internalNote) {
delete context.result.internalNote;
}
return context;
}
module.exports = {
before: {
patch: [validatePatchInput, checkMessageOwnership]
},
after: {
patch: [stripInternalFields]
}
};
In your service file, attach the hooks to the patch method:
// services/messages/messages.service.js
const { validatePatchInput, checkMessageOwnership, stripInternalFields } = require('../../hooks/message-hooks');
// Version-dependent registration example. Verify against your Feathers version.
service.hooks({
before: {
patch: [validatePatchInput, checkMessageOwnership]
},
after: {
patch: [stripInternalFields]
}
});
When a client calls patch, the pipeline runs like this:
validatePatchInputruns and throws if the payload is invalid.checkMessageOwnershiploads the message and comparesmessage.userIdtoparams.user.id.- The service method updates the record.
stripInternalFieldsremovesinternalNotefromcontext.resultbefore the response is sent.
If the ownership hook throws, the service method never runs, and any registered error hooks receive the context.
Trade-offs and limitations
Hooks introduce indirection. A reader tracing a service call must look outside the service method to understand what happens before and after it. Debugging a failed request means following the hook chain, not just reading the service file.
Async error handling is another risk. If a before hook returns a rejected promise or throws and the framework does not route it to an error hook, the failure can appear as an unhandled rejection or a silent 500. Always verify that errors thrown in hooks reach your error-handling middleware.
Reusing hooks across services also requires care. A hook that assumes context.params.user exists will break on service methods called without authentication, such as internal jobs or public endpoints. Guard against missing params or params.user before accessing properties.
Verify the pipeline before you rely on it
Run this diagnostic in your project root. No special permissions are required.
# Run in your project root.
node -e "
const { feathers } = require('@feathersjs/feathers');
const app = feathers();
app.use('/test-items', {
async get(id, params) {
return { id, label: 'sample', hidden: 'do-not-leak' };
}
});
app.service('test-items').hooks({
before: {
get: [async (context) => {
console.log('before hook ran for method:', context.method);
return context;
}]
},
after: {
get: [async (context) => {
delete context.result.hidden;
console.log('after hook stripped hidden field');
return context;
}]
}
});
app.service('test-items').get('1', { user: { id: 1 } })
.then((result) => {
console.log('final result:', JSON.stringify(result));
process.exit(0);
})
.catch((err) => {
console.error('pipeline error:', err.message);
process.exit(1);
});
"
Expected checks:
- The console prints
before hook ran for method: getbefore the service result. - The final result does not contain
hidden, confirming the after hook ran. - No unhandled rejection appears in the terminal.
Risk: mutating context.data in a before hook changes what later hooks and the service method receive. Test that downstream hooks still see the shape you expect.
Closing
Use hooks when the same concern appears across multiple service methods or transports. Keep each hook small and test it independently. If a hook grows beyond one responsibility, split it. Start by adding a single before hook to one service method, verify the execution order with the diagnostic above, then expand only where the repetition justifies the indirection.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.