FeathersJS Hooks: A Practical Place for Cross-Cutting Logic
FeathersJS hooks can keep service methods focused, but they are not free middleware. Here is when to use before, after, and error hooks, with a worked messages example.
14 Sept 2025, 21:24 UTC

The problem: cross-cutting logic scattered across service methods
In a FeathersJS API, a service method like create or find is supposed to contain domain logic. In practice, the same concerns show up in many methods: reject invalid input, attach a timestamp, remove internal fields before the response leaves the process, log the caller, and convert thrown errors into a consistent shape. Copying that code into every method makes the service harder to test and easy to break when a rule changes.
FeathersJS service hooks are the framework's answer. They let you register functions around a service method without changing the method itself. The engineering decision is not whether hooks exist; it is which concerns belong in hooks, which belong in the service method, and how to keep hook order from becoming hidden control flow.
How hooks attach and execute
Hooks are registered on a service, or globally on the app, for a specific method. The three types are before, after, and error. A before hook runs before the method and can mutate context.data or context.params. An after hook runs after the method and receives context.result. An error hook runs when the method or an earlier hook throws.
They execute in registration order. That matters: if one before hook normalizes a field and a later before hook overwrites it, the second wins. Async hooks must return a promise; FeathersJS awaits each hook before moving to the next. This is what makes async validation or a database lookup inside a hook possible, but it also means a slow hook adds latency to every request that reaches it.
Scope is another lever. You can attach a hook to one method, to every method on a service, or app-wide. Narrow scope is usually easier to reason about. App-wide hooks are appropriate for concerns that truly apply everywhere, such as attaching a request ID, but they make the request path harder to trace.
Worked example: timestamps and response shaping
The following minimal example uses the FeathersJS v5 hooks API. If you are on v4, the hook concepts are similar, but confirm the registration syntax for your version. It defines a messages service, adds a createdAt timestamp before create, and strips an internal field after find. The service methods stay focused on their core job.
// app.js — run with: node app.js
import { feathers } from '@feathersjs/feathers'
import express from '@feathersjs/express'
import socketio from '@feathersjs/socketio'
const app = express(feathers())
app.use(express.json())
app.configure(express.rest())
app.configure(socketio())
const messages = {
async create(data) {
return { id: Date.now(), ...data }
},
async find() {
return {
total: 1,
limit: 10,
skip: 0,
data: [{ id: 1, text: 'hello', internalNote: 'do not expose' }]
}
}
}
app.use('/messages', messages)
app.service('messages').hooks({
before: {
create: [
async context => {
context.data.createdAt = new Date().toISOString()
}
]
},
after: {
find: [
async context => {
const strip = record => {
const { internalNote, ...publicRecord } = record
return publicRecord
}
if (Array.isArray(context.result)) {
context.result = context.result.map(strip)
} else if (context.result && Array.isArray(context.result.data)) {
context.result.data = context.result.data.map(strip)
}
}
]
}
})
app.listen(3030)
Install the packages in a new directory and start the server:
npm init -y
npm install @feathersjs/feathers @feathersjs/express @feathersjs/socketio
node app.js
In another terminal, send a create request and then a find request:
curl -X POST http://localhost:3030/messages \
-H 'Content-Type: application/json' \
-d '{"text":"hello"}'
curl http://localhost:3030/messages
Expected checks: the create response should contain a createdAt string, and the find response should not contain internalNote. If createdAt is missing, the before hook did not run or did not mutate context.data. If internalNote is still present, check whether your service returns a paginated object with a data array and whether the after hook handles that shape. No special permissions are needed for this local example; the risk is that a broken after hook can expose fields you intended to hide, so test the response shape rather than assuming the hook ran.
Trade-offs: hooks are not free middleware
Hooks are powerful, but they are not a general-purpose middleware layer. Three limitations show up in real services.
- Order coupling. Hooks run in registration order. A later hook can overwrite an earlier hook's change. Keep each hook single-purpose, name it, and document dependencies in comments or a short test that asserts the final shape.
- Event-loop pressure. A synchronous hook that does heavy work blocks Node.js for every request. Move expensive work to an async call, a worker, or a queue. A hook that awaits a slow external service adds that latency to every matching request.
- Debugging opacity. When a request is transformed or rejected, the cause may be in any registered hook. Prefer narrow scope, log at the hook boundary with a request identifier, and avoid app-wide hooks unless the concern is genuinely global.
Use service methods for domain rules that belong to the operation itself, such as calculating a total or enforcing a state transition. Use hooks for request-scoped concerns that repeat across methods: validation, authentication context, timestamps, response shaping, and error normalization. If a concern needs to run before routing or across non-service routes, it belongs in Express middleware instead.
An actionable way to adopt hooks
Start with one hook per concern and register it on the narrowest method that needs it. Add a test that calls the service method directly and asserts the transformed result. Then remove the hook temporarily and confirm the test fails; that proves the hook is doing the work rather than the service method. Keep hook functions small, return promises for async work, and record the order in a comment when two hooks touch the same field. Finally, verify the HTTP response, not just the service return value, because hooks can change what the client sees.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.