Using FeathersJS Service Hooks to Validate and Log Todo Items
Learn how FeathersJS service hooks let you add validation and logging to a todo service without cluttering the core implementation.
15 Jun 2026, 04:17 UTC

The problem: keeping service code clean while enforcing rules
When building a REST API with FeathersJS, the generated service already handles CRUD operations. Adding validation, logging, or authentication directly inside the service file mixes concerns and makes the code harder to test and reuse. You need a way to inject cross‑cutting logic without touching the service implementation.
Thesis: service hooks let you plug async functions into the lifecycle of any service method
FeathersJS exposes before, after, and error hooks for each service path. A hook receives a context object containing params, data (for creates/updates), and result (after the method runs). By returning the context (or a rejected promise) you can short‑circuit the call, modify data, or log information—all without altering the core service.
Worked example: a todo service that validates description and logs payload
- Generate a fresh Feathers project and a todo service:
# run in your terminal, you need Node ≥14 and npm
npm init feathers-project@latest my-todo-api
cd my-todo-api
npm install
feathers generate service # choose "MongoDB" or "NeDB", name it "todo"
- Create the hook file
src/services/todo/todo.hooks.js(if it doesn’t exist) and add abeforehook forcreate:
// src/services/todo/todo.hooks.js
const { validateDescription, logPayload } = require('./todo.hooks');
module.exports = {
before: {
create: [validateDescription, logPayload],
// other methods can stay empty or use default hooks
},
after: {},
error: {}
};
function validateDescription(hook) {
const { data } = hook;
if (typeof data.description !== 'string' || data.description.trim() === '') {
throw new Error('Description must be a non‑empty string');
}
// return hook to continue the chain
return hook;
}
function logPayload(hook) {
// safe to log because we already know description is a string
console.log(`[todo.create] Incoming payload:`, hook.data);
return hook;
}
- Start the application and test the hook:
npm run dev # starts the server on http://localhost:3030
Now send a request with an invalid payload:
curl -X POST http://localhost:3030/todos \
-H "Content-Type: application/json" \
-d '{"title": "Bad todo"}' # missing description
You should see a 400 response with the error message from validateDescription, and the server log will not contain the logging line because the hook chain stopped early.
Next, send a valid payload:
curl -X POST http://localhost:3030/todos \
-H "Content-Type: application/json" \
-d '{"title": "Buy milk", "description": "Pick up 2 liters"}'
The response will be the created todo object, and the console will show a line similar to:
[todo.create] Incoming payload: { title: 'Buy milk', description: 'Pick up 2 liters' }
Trade‑off: hook ordering and explicit dependencies
Hooks run in the order they appear in the array. If a later hook expects data that an earlier hook modifies or removes, changing that order silently breaks the flow. Because the hook system does not enforce a contract, you must document any data‑flow dependencies (e.g., "logPayload assumes description is a string and therefore must run after validateDescription"). Forgetting to return the hook object (or returning undefined) in a pre‑Feathers 5.0 version can cause the service to hang; always return hook or Promise.resolve(hook).
Actionable closing: adopt hooks for any cross‑cutting concern
Start by extracting one piece of logic—validation, logging, or authentication—into a hook. Keep the hook small, pure, and testable as a separate function. Register it per‑service or globally depending on scope, and verify the behavior with a quick curl or HTTP client call as shown above. Over time, your services will stay focused on data access while hooks handle the repetitive concerns safely and transparently.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.