Decoupling Logic in Feathersjs: Using Service Hooks for Cross-Cutting Concerns
Stop bloating your Feathersjs service methods. Learn how to use Service Hooks to decouple validation, authorization, and data scrubbing from your business logic.
01 Sept 2026, 15:05 UTC

The Problem: Bloated Service Methods
When building an API, it is tempting to put everything—input validation, permission checks, and data formatting—directly inside the service method. This leads to "fat services" where the actual business logic is buried under layers of if statements checking for user roles or missing fields. If you need the same validation logic across five different services, you end up duplicating code or creating utility functions that make the service method harder to read.
The solution in Feathersjs is the Service Hook. Hooks allow you to intercept a request before it hits the database (before hooks) or after the database returns a result (after hooks), keeping your service methods focused solely on data persistence.
How Hooks Intercept the Request Flow
A hook is essentially a middleware function that receives a context object. This object is the single source of truth for the request, containing the service method being called, the parameters passed by the client, and the authenticated user.
- Before Hooks: Used for validation, authorization, and data transformation. If a before hook throws an error, the request is halted, and the service method is never executed.
- After Hooks: Used for data scrubbing (removing sensitive fields) or triggering side effects like sending an email after a record is created.
By separating these concerns, you can create a library of reusable hooks—such as verifyOwner or validateUserSchema—and apply them to any service in your application.
Implementation: Validation and Data Scrubbing
Consider a scenario where you have a messages service. You want to ensure the body is present before saving and ensure the internalNote field is never sent back to the client.
Step 1: Create the Validation Hook (Before)
Run this in your hooks file (e.g., src/services/messages/messages.hooks.js). This requires the @feathersjs/errors package to return a standard 400 Bad Request.
// before.hooks.js
import { BadRequest } from '@feathersjs/errors';
export const validateMessage = async (context) => {
const { data } = context;
if (!data.body || data.body.trim() === '') {
throw new BadRequest('Message body is required');
}
return context;
};
Step 2: Create the Scrubbing Hook (After)
// after.hooks.js
export const removeInternalNotes = async (context) => {
// context.result contains the data returned from the database
if (context.result && context.result.internalNote) {
delete context.result.internalNote;
}
return context;
};
Step 3: Register the Hooks
// messages.hooks.js
export default {
before: {
create: [validateMessage],
update: [validateMessage],
},
after: {
get: [removeInternalNotes],
find: [removeInternalNotes],
}
};
Trade-offs and Debugging Challenges
While hooks promote a clean architecture, they introduce a "hidden" execution flow. When a request fails, it isn't always immediately obvious which hook in a chain of ten caused the error. This is known as the hook chain complexity problem.
To manage this, follow these guidelines:
- Keep hooks atomic: One hook should do one thing (e.g.,
verifyAuthshould not also validate the schema). - Be mindful of async operations: Always use
async/await. If a hook performs an asynchronous task but doesn't return a Promise, Feathers will move to the next hook before the task completes, leading to race conditions. - Context Mutation: Modifying
context.datain a before hook changes what is written to the database. Always verify the final state of the data if you have multiple transformation hooks.
Verification Checklist
To verify your hooks are working as intended:
- Test Validation: Send a POST request with an empty body. You should receive a
400 Bad Requestand the database should contain no new record. - Test Scrubbing: Manually insert a record with an
internalNotevia the database console. Perform a GET request via the API; theinternalNotefield should be absent from the JSON response.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.