Decoupling Business Logic with FeathersJS Service Hooks
Learn how to use FeathersJS Service Hooks to move validation and authorization out of your business logic and into a reusable, declarative pipeline.
29 Nov 2025, 00:08 UTC

The Problem: Bloated Service Methods
When building a REST or WebSocket API, it is common to start by placing validation, authorization, and data transformation directly inside the service method. As the application grows, a simple create method becomes a 200-line monolith. This makes the code difficult to test, impossible to reuse across different transports, and prone to bugs when a single change in validation logic breaks multiple endpoints.
The solution is to treat your service as a "pure" data handler and move cross-cutting concerns into Service Hooks. Hooks allow you to wrap service methods in a declarative pipeline, ensuring that logic like "only admins can delete users" happens before the database is ever touched.
How the Hook Pipeline Works
In FeathersJS, a Service is an abstraction that doesn't care if a request came from an HTTP call or a Socket.io event. Hooks are middleware functions that intercept these calls at three specific stages:
- Before: Used for validation, authorization, and modifying input parameters.
- After: Used for data scrubbing (removing passwords), triggering notifications, or formatting the response.
- Error: Used for custom error handling or logging before the response is sent to the client.
Each hook receives a context object. To change the data going into the service, you modify context.params. To change the data returning to the client, you modify context.result.
Example: Implementing a Secure User Creation Flow
Consider a scenario where you need to validate a user's email, ensure they aren't creating a duplicate account, and hash their password before saving to the database. Instead of putting this in the service, you chain reusable hooks.
1. The Validation Hook
// src/hooks/validate-user.js
export const validateUser = async (context) => {
const { data } = context;
if (!data.email || !data.email.includes('@')) {
throw new Error('A valid email is required');
}
return context;
};
2. The Data Transformation Hook
// src/hooks/hash-password.js
import bcrypt from 'bcryptjs';
export const hashPassword = async (context) => {
const { data } = context;
if (data.password) {
data.password = await bcrypt.hash(data.password, 10);
}
return context;
};
3. Registering the Pipeline
In your service configuration (typically services/users/users.hooks.js), you apply these hooks to the create method. Run these within the Feathers application environment with administrative permissions to modify the service definition.
// src/services/users/users.hooks.js
import { validateUser } from '../../hooks/validate-user';
import { hashPassword } from '../../hooks/hash-password';
export default {
before: {
create: [validateUser, hashPassword],
// Other methods like patch or update would go here
},
after: {
create: [async (context) => {
// Remove password from the result so it isn't sent back to the client
delete context.result.password;
return context;
}]
}
};
Trade-offs: The "Hidden Logic" Trap
While hooks provide a clean separation of concerns, they introduce a risk of fragmented visibility. When a developer looks at the service file, they see a simple database call, but the actual behavior is scattered across five different hook files. This can make debugging a request flow difficult if the pipeline becomes too long.
To mitigate this, avoid creating "generic" hooks that do too many things. Keep each hook focused on one task (e.g., checkAdminRole rather than handleSecurity) and maintain a clear naming convention that describes exactly what the hook modifies.
Verifying the Implementation
To verify that your hooks are executing correctly, you can use a tool like curl or Postman to send a POST request to your service endpoint.
- Check Validation: Send a request missing the email field; you should receive the custom error defined in
validateUser. - Check Transformation: Send a valid request and check the database directly. The password should be hashed, not plain text.
- Check Scrubbing: Inspect the JSON response from the API; the
passwordfield should be absent from the result.
Rollback: To revert these changes, remove the hook references from the hooks.js file of the service and restart the server. This restores the service to its base behavior without affecting the underlying database schema.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.