What are Hooks?
Hooks are middleware functions that run before, after, or around service methods. They allow you to add validation, authorization, data transformation, and other business logic in a composable and reusable way.
Hook Types
Feathers supports four types of hooks:
before - Run before the service method
after - Run after the service method completes successfully
error - Run when the service method throws an error
around - Wrap the entire service method call
Basic Hook Usage
Register hooks on a service using the .hooks() method:
Before Hooks
After Hooks
Error Hooks
app . service ( 'messages' ). hooks ({
before: {
// Run before all methods
all: [
async ( context ) => {
console . log ( 'Before all methods' )
}
],
// Run only before create
create: [
async ( context ) => {
context . data . createdAt = new Date ()
}
],
// Run only before find and get
find: [
async ( context ) => {
console . log ( 'Finding messages' )
}
],
get: [
async ( context ) => {
console . log ( `Getting message ${ context . id } ` )
}
]
}
})
Hook Context
Every hook receives a context object containing information about the service call:
const logContext = async ( context ) => {
console . log ({
app: context . app , // The Feathers application
service: context . service , // The service this hook is for
path: context . path , // The service path
method: context . method , // The service method name
type: context . type , // Hook type: 'before', 'after', 'error'
id: context . id , // The id for get, update, patch, remove
data: context . data , // Data for create, update, patch
params: context . params , // Service call parameters
result: context . result , // The result (after hooks only)
error: context . error , // The error (error hooks only)
statusCode: context . statusCode // HTTP status code (for REST)
})
}
app . service ( 'messages' ). hooks ({
before: {
all: [ logContext ]
}
})
Modifying the Context
Hooks can modify the context to change behavior:
Modify Data
Modify Query
Modify Result
app . service ( 'messages' ). hooks ({
before: {
create: [
async ( context ) => {
// Add timestamps
context . data = {
... context . data ,
createdAt: new Date ()
}
}
],
patch: [
async ( context ) => {
// Add updated timestamp
context . data . updatedAt = new Date ()
}
]
}
})
Application-Level Hooks
Register hooks that run for all services:
app . hooks ({
before: {
all: [
async ( context ) => {
console . log ( `Calling ${ context . path } . ${ context . method } ` )
}
],
create: [
async ( context ) => {
// Add creation timestamp to all creates
context . data . createdAt = new Date ()
}
]
},
after: {
all: [
async ( context ) => {
console . log ( `Completed ${ context . path } . ${ context . method } ` )
}
]
},
error: {
all: [
async ( context ) => {
console . error ( `Error in ${ context . path } . ${ context . method } :` , context . error )
}
]
}
})
Around Hooks
Around hooks wrap the entire service method call and give you control over the execution flow:
app . service ( 'messages' ). hooks ({
around: {
all: [
async ( context , next ) => {
console . log ( 'Before method' )
// Call the next hook or service method
await next ()
console . log ( 'After method' )
}
],
find: [
// Timing hook
async ( context , next ) => {
const start = Date . now ()
await next ()
const duration = Date . now () - start
console . log ( `find() took ${ duration } ms` )
},
// Caching hook
async ( context , next ) => {
const cacheKey = JSON . stringify ( context . params . query )
const cached = cache . get ( cacheKey )
if ( cached ) {
context . result = cached
return // Skip calling next()
}
await next ()
cache . set ( cacheKey , context . result )
}
]
}
})
Around hooks must call await next() to continue execution. If you don’t call next(), the service method won’t execute.
Common Hook Patterns
Validation Hooks
Validate data before it reaches the service:
import { BadRequest } from '@feathersjs/errors'
const validateMessage = async ( context ) => {
const { data } = context
if ( ! data . text || data . text . trim (). length === 0 ) {
throw new BadRequest ( 'Message text is required' )
}
if ( data . text . length > 500 ) {
throw new BadRequest ( 'Message text must be 500 characters or less' )
}
}
app . service ( 'messages' ). hooks ({
before: {
create: [ validateMessage ],
update: [ validateMessage ],
patch: [ validateMessage ]
}
})
Authorization Hooks
Check permissions before allowing operations:
import { Forbidden , NotAuthenticated } from '@feathersjs/errors'
const requireAuth = async ( context ) => {
if ( ! context . params . user ) {
throw new NotAuthenticated ( 'You must be logged in' )
}
}
const requireOwner = async ( context ) => {
const message = await context . service . get ( context . id )
if ( message . userId !== context . params . user . id ) {
throw new Forbidden ( 'You can only modify your own messages' )
}
}
app . service ( 'messages' ). hooks ({
before: {
create: [ requireAuth ],
patch: [ requireAuth , requireOwner ],
remove: [ requireAuth , requireOwner ]
}
})
Data Sanitization
Clean and transform data:
const sanitizeData = async ( context ) => {
const { data } = context
// Trim strings
if ( data . text ) {
data . text = data . text . trim ()
}
if ( data . title ) {
data . title = data . title . trim ()
}
// Remove disallowed fields
delete data . id
delete data . createdAt
delete data . userId
}
app . service ( 'messages' ). hooks ({
before: {
create: [ sanitizeData ],
update: [ sanitizeData ],
patch: [ sanitizeData ]
}
})
Adding User Context
Automatically associate records with the current user:
const setUserId = async ( context ) => {
context . data . userId = context . params . user . id
}
const limitToUser = async ( context ) => {
// Only show the current user's messages
context . params . query . userId = context . params . user . id
}
app . service ( 'messages' ). hooks ({
before: {
create: [ setUserId ],
find: [ limitToUser ]
}
})
Schema-Based Hooks
Feathers provides schema-based validation and resolution hooks:
Validation Hooks
Resolution Hooks
import { validateQuery , validateData } from '@feathersjs/schema'
import { Type } from '@sinclair/typebox'
const messageSchema = Type . Object ({
text: Type . String ({ minLength: 1 , maxLength: 500 }),
userId: Type . Number ()
})
const querySchema = Type . Object ({
userId: Type . Optional ( Type . Number ()),
$limit: Type . Optional ( Type . Number ())
})
app . service ( 'messages' ). hooks ({
before: {
find: [ validateQuery ( querySchema )],
create: [ validateData ( messageSchema )]
}
})
Hook Execution Order
Hooks execute in a specific order:
Application-Level Around Hooks
Around hooks registered at the application level run first
Service-Level Around Hooks
Around hooks registered at the service level
Application-Level Before Hooks
Before hooks from app.hooks()
Service-Level Before Hooks
Before hooks from service.hooks()
Service Method
The actual service method executes
Service-Level After Hooks
After hooks from service.hooks()
Application-Level After Hooks
After hooks from app.hooks()
Within each category, hooks run in the order they were registered.
Conditional Hooks
Run hooks only under certain conditions:
const conditionalHook = async ( context ) => {
// Only run for REST API calls
if ( context . params . provider === 'rest' ) {
console . log ( 'Called via REST' )
}
// Only run for authenticated users
if ( context . params . user ) {
console . log ( 'Authenticated user:' , context . params . user . id )
}
// Only run for specific methods
if ( context . method === 'create' ) {
context . data . createdAt = new Date ()
}
}
Async Hook Registration
You can register hooks as arrays for cleaner composition:
app . service ( 'messages' ). hooks ([
async ( context , next ) => {
console . log ( 'First hook' )
await next ()
},
async ( context , next ) => {
console . log ( 'Second hook' )
await next ()
}
])
Error Handling in Hooks
Handle errors gracefully in your hooks:
import { GeneralError } from '@feathersjs/errors'
app . hooks ({
error: {
all: [
async ( context ) => {
// Log the error
console . error ( `Error in ${ context . path } . ${ context . method } :` , context . error )
// Convert unknown errors to GeneralError
if ( ! context . error . code ) {
context . error = new GeneralError ( context . error . message )
}
// Don't expose internal errors to clients
if ( context . params . provider && context . error . code === 500 ) {
context . error . message = 'An internal error occurred'
}
}
]
}
})
Complete Hook Example
Here’s a complete example with validation, authorization, and data transformation:
import { BadRequest , Forbidden , NotAuthenticated } from '@feathersjs/errors'
// Validation
const validateMessage = async ( context ) => {
const { data } = context
if ( ! data . text ?. trim ()) {
throw new BadRequest ( 'Message text is required' )
}
if ( data . text . length > 500 ) {
throw new BadRequest ( 'Message text too long' )
}
}
// Authorization
const requireAuth = async ( context ) => {
if ( ! context . params . user ) {
throw new NotAuthenticated ( 'Authentication required' )
}
}
const requireOwner = async ( context ) => {
const message = await context . service . get ( context . id )
if ( message . userId !== context . params . user . id ) {
throw new Forbidden ( 'Not authorized' )
}
}
// Data enrichment
const addUserData = async ( context ) => {
context . data = {
... context . data ,
userId: context . params . user . id ,
createdAt: new Date ()
}
}
// Sanitization
const sanitize = async ( context ) => {
context . data . text = context . data . text . trim ()
delete context . data . id
delete context . data . userId
}
// Result transformation
const removePassword = async ( context ) => {
if ( context . result ) {
const results = Array . isArray ( context . result . data )
? context . result . data
: [ context . result ]
results . forEach ( item => {
delete item . password
})
}
}
// Register all hooks
app . service ( 'messages' ). hooks ({
before: {
all: [ requireAuth ],
create: [ sanitize , validateMessage , addUserData ],
patch: [ sanitize , validateMessage , requireOwner ],
remove: [ requireOwner ]
},
after: {
all: [ removePassword ]
},
error: {
all: [
async ( context ) => {
console . error ( 'Error:' , context . error )
}
]
}
})