Stop Repeating Yourself in FeathersJS: Centralize Validation and Logging with Service Hooks
FeathersJS hooks let you move validation, sanitization, and logging out of service methods into a reusable before/after/error pipeline. Here's a worked sanitize-input example, plus the ordering and latency trade-offs to watch.
31 Oct 2025, 22:24 UTC

If your FeathersJS services all start with the same five lines — trim the input, check the user, log the call — you don't have five services, you have one concern copy-pasted five times. The fix is the feature Feathers was built around: hooks, a declarative pipeline that wraps every service method with reusable before/after/error logic.
The problem hooks actually solve
Every Feathers service exposes methods like find, get, create, update, patch, and remove. Cross-cutting concerns — validation, sanitization, authorization, logging — apply to many of those methods across many services. Inline, that logic drifts: one service trims whitespace, another forgets, a third sanitizes HTML differently.
Hooks let you register functions that run around the service method, in three phases: before (inspect or mutate the incoming data), after (shape the result), and error (react to failures). Each hook receives a context object — containing context.data, context.params, context.result, the authenticated user, and more — and returns it (or a modified copy) to pass control down the chain.
A worked example: a sanitize-input hook
Suppose users can post messages, and you want every created or patched message to have trimmed strings and no HTML tags. Create src/hooks/sanitize-input.js:
// src/hooks/sanitize-input.js
const stripTags = (value) =>
typeof value === 'string' ? value.replace(/<[^>]*>/g, '').trim() : value;
module.exports = function sanitizeInput(fields) {
return async (context) => {
if (!context.data) return context;
for (const field of fields) {
if (field in context.data) {
context.data[field] = stripTags(context.data[field]);
}
}
return context; // always return the context
};
};Then register it on the messages service, typically in src/services/messages/messages.hooks.js (or wherever your generated service registers hooks):
const sanitizeInput = require('../../hooks/sanitize-input');
module.exports = {
before: {
create: [sanitizeInput(['text', 'author'])],
patch: [sanitizeInput(['text'])]
},
after: {},
error: {}
};Now any client hitting POST /messages with { "text": " <b>hi</b> " } stores "hi" — and the service method itself never had to know. The same hook can be reused on a comments service with a different field list.
Verify it ran
Start the app with npm start (run from the project root; no special permissions needed for local development) and send a request:
curl -X POST http://localhost:3030/messages \
-H "Content-Type: application/json" \
-d '{ "text": " <script>x</script>hello " }'Check the response body: text should be "hello". If you also add a temporary console.log(context.data) inside the hook, you should see it fire before the service method's own logs. Port 3030 is the Feathers default; adjust if you configured otherwise.
Trade-offs worth knowing
- Always return the context. A hook that returns
undefinedcan break the chain in confusing ways. Async hooks should return the context (or a promise resolving to it). - Ordering matters. Hooks run in registration order. Authentication before authorization, sanitize before validate — a misplaced hook can silently operate on stale data.
- Latency adds up. Hooks that hit a database or external API run on every matched call. Keep them cheap, or scope them to specific methods rather than
all. - Don't bury business logic here. Hooks shine for cross-cutting concerns. If a hook encodes core domain rules, it becomes invisible to anyone reading the service — and harder to unit test.
Where to start
Pick one concern you've duplicated across at least two services — logging the caller, trimming input, stamping createdBy — and extract it into a single hook file. Register it, hit the endpoint with curl, and confirm the behavior moved without changing the outcome. Then repeat. Within a few iterations, your service methods contain only the logic that actually differs between them — which is the whole point.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.