One Service, Many Transports: Centralizing Logic with FeathersJS Hooks
Use FeathersJS service hooks to centralize validation, authorization and transformation so REST and real-time clients share the same business logic without duplication.
07 Nov 2025, 09:05 UTC

Building a backend that must serve both REST clients and real-time subscribers often leads to duplicated validation, authorization and data transformation code. The useful takeaway is to place that logic in FeathersJS service hooks so every transport executes the same pipeline.
Service definition and transport‑agnostic methods
A Feathers service is a plain object that implements any subset of the CRUD methods (find, get, create, patch, remove). The framework maps HTTP verbs to those methods for the REST adapter and also translates WebSocket messages to the same method calls. Because the mapping is done by the framework, you do not need separate handlers for REST and sockets.
Typical registration looks like:
// src/app.js
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const socketio = require('@feathersjs/socketio');
const app = express(feathers())
.configure(socketio())
.configure(express.rest());
app.use('/messages', {
async create(data, params) {
// core business logic lives here
return { id: Date.now(), ...data, createdAt: new Date() };
},
async find(params) {
return [];
}
});
Run this in a Node project where you have write access to the src folder. The risk of omitting hooks is that validation and authorization would be missing for both REST and real‑time calls.
Hook pipeline order and context shape
Feathers wraps each service method in a three‑stage hook pipeline: all before hooks, then the service method itself, then all after hooks. Each hook receives a context object that contains at least method, id, data, result and params. Modifying data in a before hook changes what the service method sees; modifying result in an after hook changes the value returned to REST callers and the payload emitted to real‑time listeners.
The exact shape of the context object varies between FeathersJS v4 and v5 (e.g., v5 adds a type field and changes how params.provider is populated). Check package.json for the installed version and compare the hook signature in the official documentation before adopting.
Real‑time publishing via service events
When a service method resolves successfully, Feathers emits an event named after the method (created, updated, patched, removed). Real‑time adapters (such as the Socket.io provider) subscribe to these events and push the payload to connected clients. The publish function determines which channels receive which events; defaults differ between adapters, so verify the configuration for your chosen transport.
Worked example: messages service with sanitization and audit logging
Goal: trim whitespace from the text field, reject empty messages, and write an audit log entry after the message is stored, without duplicating the logic for REST and WebSocket callers.
// src/messages.hooks.js
const trimText = async (context) => {
if (context.data?.text) {
context.data.text = String(context.data.text).trim();
}
return context;
};
const rejectEmpty = async (context) => {
if (!context.data?.text || context.data.text.length === 0) {
throw new Error('Message text must not be empty');
}
return context;
};
const auditLog = async (context) => {
if (context.result) {
// In a real app you would write to a database or external service
console.log(`[AUDIT] Message ${context.result.id} created by ${context.params.user?.id ?? 'anonymous'}`);
}
return context;
};
module.exports = {
before: {
create: [trimText, rejectEmpty]
},
after: {
create: [auditLog]
}
};
Attach the hooks to the service:
// src/app.js (continued)
const messagesHooks = require('./messages.hooks');
app.service('messages').hooks(messagesHooks);
The same before hooks run for a POST /messages request and for a Socket.io messages.create call. The after hook runs before the HTTP response is sent and before the created event is emitted, so audit log entries appear for both transports and clients never see unsanitized input.
Trade‑off and limitation
Centralizing logic in hooks improves reuse and guarantees consistency across transports, but it can make the control flow harder to follow. A long hook chain couples unrelated concerns and adds overhead if each hook performs expensive work. Keeping hooks small, side‑effect‑free where possible, and well‑tested mitigates these issues. Adding a logger at the start and end of each hook (e.g., console.debug with the hook name and context method) helps trace execution.
Actionable closing
To verify that hooks are shared:
- Create a minimal Feathers app with an in‑memory service (or a NeDB adapter) and attach the before/after hooks shown above.
- Start the app and call the service via REST (for example, using curl or Postman) and via a Socket.io client.
- Observe the console output: the
trimTexthook should process the trimmed text, therejectEmptyhook should allow non‑empty messages, and theauditLoghook should print a line for each call. - Check that the REST response and the Socket.io event payload both contain the trimmed
textfield and lack leading/trailing spaces. - Review the service event names (
created) and the publish configuration (default publishes to everyone) to confirm which clients receive the event.
Use hooks for validation, authorization, transformation and side effects that must be identical across transports. Keep transport‑specific concerns (e.g., setting custom HTTP headers) out of hooks and in the adapter or middleware layer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.