Hook Types
Feathers supports four types of hooks:declarations.ts:359
type HookType = 'before' | 'after' | 'error' | 'around'
- Before
- After
- Error
- Around
Before hooks run before the service method:Use cases:
service.hooks({
before: {
create: [
async (context) => {
// Validate data
if (!context.data.email) {
throw new BadRequest('Email is required')
}
// Modify data
context.data.createdAt = new Date()
return context
}
]
}
})
- Input validation
- Authentication checks
- Data transformation
- Setting default values
After hooks run after successful service method calls:Use cases:
service.hooks({
after: {
find: [
async (context) => {
// Modify result
context.result.data = context.result.data.map(item => ({
...item,
fullName: `${item.firstName} ${item.lastName}`
}))
return context
}
]
}
})
- Data transformation
- Adding computed fields
- Filtering sensitive data
- Logging successful operations
Error hooks run when a service method throws an error:Use cases:
service.hooks({
error: {
all: [
async (context) => {
console.error('Error in', context.path, context.method)
console.error(context.error)
// Transform error
if (context.error.code === 'ECONNREFUSED') {
context.error = new Unavailable('Database unavailable')
}
return context
}
]
}
})
- Error logging
- Error transformation
- Sending notifications
- Cleanup operations
Around hooks wrap the method execution and have full control:Use cases:
service.hooks({
around: {
all: [
async (context, next) => {
const start = Date.now()
try {
await next() // Call next hooks and method
const duration = Date.now() - start
console.log(`${context.method} took ${duration}ms`)
} catch (error) {
console.error('Error:', error)
throw error
}
}
]
}
})
- Performance monitoring
- Transaction management
- Caching
- Custom error handling
Hook Context
Every hook receives a context object with information about the service call:declarations.ts:363-444
interface HookContext {
app: Application // The Feathers application
service: Service // The service being called
path: string // Service path (e.g., 'users')
method: string // Method name (e.g., 'create')
type: HookType // 'before', 'after', 'error', 'around'
params: Params // Service method parameters
id?: Id // The record ID (for get, update, patch, remove)
data?: any // Data being created/updated (for create, update, patch)
result?: any // Method result (available in 'after' hooks)
error?: Error // Error object (available in 'error' hooks)
event: string | null // Event name to emit
dispatch?: any // Data to send to client (alternative to result)
http?: Http // HTTP-specific properties
}
service.hooks({
before: {
get: [
async (context) => {
console.log('App:', context.app)
console.log('Service:', context.path)
console.log('Method:', context.method)
console.log('ID:', context.id)
console.log('Params:', context.params)
console.log('User:', context.params.user)
}
]
}
})
service.hooks({
before: {
create: [
async (context) => {
// Modify data
context.data.createdAt = new Date()
context.data.userId = context.params.user.id
// Add to params
context.params.timestamp = Date.now()
return context
}
]
},
after: {
all: [
async (context) => {
// Modify result
if (context.result) {
delete context.result.password
}
return context
}
]
}
})
Registering Hooks
Service Hooks
Register hooks for specific service methods:hooks.ts:214-234
service.hooks({
before: {
all: [hook1, hook2], // Run for all methods
find: [hook3], // Run only for find
get: [hook4],
create: [hook5, hook6],
update: [hook7],
patch: [hook8],
remove: [hook9]
},
after: {
all: [afterHook1],
create: [afterHook2]
},
error: {
all: [errorHook1]
},
around: {
all: [aroundHook1]
}
})
Application Hooks
Register hooks that run for all services:application.ts:206-223
app.hooks({
before: {
all: [
async (context) => {
console.log(`Calling ${context.path}.${context.method}`)
}
]
},
after: {
all: [
async (context) => {
console.log('Success!')
}
]
},
error: {
all: [
async (context) => {
console.error('Error:', context.error.message)
}
]
}
})
Multiple Registration Formats
- Standard Format
- Around Only
- Method-Specific
- Chainable
service.hooks({
before: {
all: [hook1, hook2],
create: [hook3]
},
after: {
all: [hook4]
}
})
service.hooks({
around: {
all: [aroundHook1, aroundHook2]
}
})
service.hooks({
find: [middleware1, middleware2],
create: [middleware3]
})
service
.hooks({
before: { all: [hook1] }
})
.hooks({
after: { all: [hook2] }
})
Hook Execution Order
Hooks execute in a specific order:hooks.ts:159-164
1. Application around hooks (all)
2. Application around hooks (method-specific)
3. Application before hooks (all)
4. Application before hooks (method-specific)
5. Service around hooks (all)
6. Service around hooks (method-specific)
7. Service before hooks (all)
8. Service before hooks (method-specific)
9. Service method executes
10. Service after hooks (method-specific)
11. Service after hooks (all)
12. Application after hooks (method-specific)
13. Application after hooks (all)
If an error occurs at any point, the execution switches to error hooks in reverse order.
Common Hook Patterns
Authentication
const authenticate = async (context) => {
const { params } = context
if (!params.provider) {
// Internal call, skip authentication
return context
}
const token = params.headers?.authorization?.replace('Bearer ', '')
if (!token) {
throw new NotAuthenticated('No token provided')
}
try {
const decoded = verifyToken(token)
context.params.user = decoded
return context
} catch (error) {
throw new NotAuthenticated('Invalid token')
}
}
service.hooks({
before: {
all: [authenticate]
}
})
Authorization
const authorize = (...allowedRoles) => {
return async (context) => {
const { user } = context.params
if (!user) {
throw new NotAuthenticated('Not authenticated')
}
if (!allowedRoles.includes(user.role)) {
throw new Forbidden(`Role '${user.role}' not allowed`)
}
return context
}
}
service.hooks({
before: {
create: [authorize('admin', 'editor')],
remove: [authorize('admin')]
}
})
Validation
const validate = (schema) => {
return async (context) => {
try {
context.data = await schema.validate(context.data, {
stripUnknown: true
})
return context
} catch (error) {
throw new BadRequest('Validation failed', {
errors: error.errors
})
}
}
}
const userSchema = {
async validate(data, options) {
if (!data.email) throw new Error('Email required')
if (!data.password) throw new Error('Password required')
return data
}
}
service.hooks({
before: {
create: [validate(userSchema)]
}
})
Remove Fields
const removeFields = (...fields) => {
return async (context) => {
if (context.result) {
if (Array.isArray(context.result)) {
context.result = context.result.map(item => {
const copy = { ...item }
fields.forEach(field => delete copy[field])
return copy
})
} else if (context.result.data) {
// Paginated results
context.result.data = context.result.data.map(item => {
const copy = { ...item }
fields.forEach(field => delete copy[field])
return copy
})
} else {
fields.forEach(field => delete context.result[field])
}
}
return context
}
}
service.hooks({
after: {
all: [removeFields('password', 'ssn', 'creditCard')]
}
})
Populate Associations
const populate = (field, service) => {
return async (context) => {
const { app, result } = context
if (!result) return context
const items = Array.isArray(result) ? result :
result.data ? result.data : [result]
await Promise.all(
items.map(async (item) => {
if (item[`${field}Id`]) {
item[field] = await app.service(service).get(item[`${field}Id`])
}
})
)
return context
}
}
service.hooks({
after: {
find: [populate('author', 'users')],
get: [populate('author', 'users')]
}
})
Soft Delete
const softDelete = async (context) => {
// Change remove to patch
context.data = {
deletedAt: new Date(),
deleted: true
}
const result = await context.service.patch(context.id, context.data, context.params)
context.result = result
// Skip actual remove by setting result
return context
}
const excludeDeleted = async (context) => {
// Add deleted filter to query
context.params.query = {
...context.params.query,
deleted: { $ne: true }
}
return context
}
service.hooks({
before: {
find: [excludeDeleted],
get: [excludeDeleted],
remove: [softDelete]
}
})
Caching
const cache = new Map()
const cacheResult = async (context, next) => {
const { id, method, path } = context
if (method !== 'get') {
return next()
}
const cacheKey = `${path}:${id}`
if (cache.has(cacheKey)) {
console.log('Cache hit:', cacheKey)
context.result = cache.get(cacheKey)
return context
}
await next()
if (context.result) {
cache.set(cacheKey, context.result)
}
return context
}
const invalidateCache = async (context) => {
const { id, method, path } = context
const cacheKey = `${path}:${id}`
if (['create', 'update', 'patch', 'remove'].includes(method)) {
cache.delete(cacheKey)
}
return context
}
service.hooks({
around: {
get: [cacheResult]
},
after: {
create: [invalidateCache],
update: [invalidateCache],
patch: [invalidateCache],
remove: [invalidateCache]
}
})
Skipping Service Methods
Setcontext.result in a before hook to skip the actual service method:
service.hooks({
before: {
get: [
async (context) => {
// Check cache
const cached = await cache.get(context.id)
if (cached) {
// Skip service method by setting result
context.result = cached
}
return context
}
]
}
})
Error Handling in Hooks
Throwing Errors
service.hooks({
before: {
create: [
async (context) => {
if (!context.data.email) {
throw new BadRequest('Email is required')
}
}
]
}
})
Handling Errors
hooks.ts:39-50
service.hooks({
error: {
all: [
async (context) => {
// Log error
console.error(`Error in ${context.path}.${context.method}:`, context.error)
// Transform error
if (context.error.code === 'ECONNREFUSED') {
context.error = new Unavailable('Service unavailable')
}
// Or recover from error
if (context.method === 'get' && context.error.code === 404) {
context.result = null // Return null instead of throwing
delete context.error // Clear error
}
return context
}
]
}
})
TypeScript Support
import { HookContext, NextFunction } from '@feathersjs/feathers'
interface User {
id: number
email: string
role: string
}
const authenticate = async (context: HookContext): Promise<HookContext> => {
// Hook implementation
return context
}
const authorize = (...roles: string[]) => {
return async (context: HookContext<Application, UserService>): Promise<HookContext> => {
const user = context.params.user as User
if (!roles.includes(user.role)) {
throw new Forbidden('Access denied')
}
return context
}
}
const timing = async (context: HookContext, next: NextFunction): Promise<void> => {
const start = Date.now()
await next()
console.log(`Duration: ${Date.now() - start}ms`)
}
Best Practices
- Keep hooks focused - Each hook should do one thing well
- Make hooks reusable - Use factory functions for configurable hooks
- Order matters - Place authentication/authorization first
- Return context - Always return the context object (except around hooks)
- Use async/await - Avoid promise chains
- Handle errors properly - Use error hooks for cleanup
- Avoid side effects - Be careful with mutations
- Test hooks independently - Unit test hooks separately from services
Next Steps
Events
Learn about real-time events
Errors
Master error handling in Feathers