Hooks ​
When we created our messages service in the services chapter, we saw that Feathers services are a great way to implement data storage and modification. Technically, we could write our entire app with services but very often we need similar functionality across multiple services. For example, we might want to check for all services if a user is allowed to access it. With just services, we would have to write this every time.
This is where Feathers hooks come in. Hooks are pluggable middleware functions that can be registered around, before, after or on errors of a service method without changing the original code.
Just like services themselves, hooks are transport independent. They are usually also service independent, meaning they can be used with ​any​ service. This pattern keeps your application logic flexible, composable, and much easier to trace through and debug. Hooks are commonly used to handle things like validation, authorization, logging, sending emails and more.
tip
A full overview of the hook API can be found in the hooks API documentation. For the general design pattern behind hooks see this blog post.
Generating a hook ​
Let's generate a hook that logs the total runtime of a service method to the console.
npx feathers generate hook
We call our hook log-runtime
and confirm the type with enter to make it an around
hook.
Now update src/hooks/log-runtime.ts
as follows:
Now update src/hooks/log-runtime.js
as follows:
import type { HookContext, NextFunction } from '../declarations'
import { logger } from '../logger'
export const logRuntime = async (context: HookContext, next: NextFunction) => {
const startTime = Date.now()
// Run everything else (other hooks and service call)
await next()
const duration = Date.now() - startTime
logger.info(`Calling ${context.method} on ${context.path} took ${duration}ms`)
}
In this hook, we store the start time and then run all other hooks and the service method by calling await next()
. After that we can calculate the duration in milliseconds by subtracting the start time from the current time and log the information using the application logger.
Hook functions ​
A hook function is an async
function that takes the hook context
and a next
function as the parameter. If the hook should only run on error, before or after the service method, it does not need a next
function. However since we need to do both, get the start time before and the end time after, we created an around
hook.
Hooks run in the order they are registered and if a hook function throws an error, all remaining hooks (and the service call if it didn't run yet) will be skipped and the error will be returned.
Hook context ​
The hook context
is an object which contains information about the service method call. It has read-only and writable properties.
Read-only properties are:
context.app
- The Feathers application object. This commonly used to call other servicescontext.service
- The service object this hook is currently running oncontext.path
- The path (name) of the servicecontext.method
- The name of the service method being calledcontext.type
- The hook type (around, before, etc)
Writeable properties are:
context.params
- The service method callparams
. For external calls,params
usually contains:context.params.query
- The query filter (e.g. from the REST query string) for the service callcontext.params.provider
- The name of the transport the call has been made through. Usually"rest"
or"socketio"
. Will beundefined
for internal calls.context.params.user
- If authenticated, the data of the user making the service method call.
context.id
- Theid
of the record if the service method call is aget
,remove
,update
orpatch
context.data
- Thedata
sent by the user in acreate
,update
andpatch
and custom service method callcontext.error
- The error that was thrown (inerror
hooks)context.result
- The result of the service method call (available after callingawait next()
or inafter
hooks)
tip
For more information about the hook context see the hooks API documentation.
Registering hooks ​
In a Feathers application, hooks are being registered in the <servicename> file. The hook registration object is an object with { around, before, after, error }
and a list of hooks per method like { all: [], find: [], create: [] }
.
To log the runtime of our messages
service calls we can update src/services/messages/messages.ts
like this:
To log the runtime of all messages
service calls we can update src/services/messages/messages.js
like this:
// For more information about this file see https://dove.feathersjs.com/guides/cli/service.html
import { authenticate } from '@feathersjs/authentication'
import { hooks as schemaHooks } from '@feathersjs/schema'
import {
messageDataValidator,
messagePatchValidator,
messageQueryValidator,
messageResolver,
messageExternalResolver,
messageDataResolver,
messagePatchResolver,
messageQueryResolver
} from './messages.schema'
import type { Application } from '../../declarations'
import { MessageService, getOptions } from './messages.class'
import { messagePath, messageMethods } from './messages.shared'
import { logRuntime } from '../../hooks/log-runtime'
export * from './messages.class'
export * from './messages.schema'
// A configure function that registers the service and its hooks via `app.configure`
export const message = (app: Application) => {
// Register our service on the Feathers application
app.use(messagePath, new MessageService(getOptions(app)), {
// A list of all methods this service exposes externally
methods: messageMethods,
// You can add additional custom events to be sent to clients here
events: []
})
// Initialize hooks
app.service(messagePath).hooks({
around: {
all: [
logRuntime,
authenticate('jwt'),
schemaHooks.resolveExternal(messageExternalResolver),
schemaHooks.resolveResult(messageResolver)
]
},
before: {
all: [schemaHooks.validateQuery(messageQueryValidator), schemaHooks.resolveQuery(messageQueryResolver)],
find: [],
get: [],
create: [schemaHooks.validateData(messageDataValidator), schemaHooks.resolveData(messageDataResolver)],
patch: [schemaHooks.validateData(messagePatchValidator), schemaHooks.resolveData(messagePatchResolver)],
remove: []
},
after: {
all: []
},
error: {
all: []
}
})
}
// Add this service to the service type index
declare module '../../declarations' {
interface ServiceTypes {
[messagePath]: MessageService
}
}
Now every time our messages service is accessed successfully, the name, method and runtime will be logged.
tip
all
is a special keyword which means those hooks will run before the method specific hooks. Method specific hooks can be registered based on their name, e.g. to only log the runtime for find
and get
:
app.service('messages').hooks({
around: {
all: [authenticate('jwt')],
find: [logRuntime],
get: [logRuntime]
}
// ...
})
What's next? ​
In this chapter we learned how Feathers hooks can be used as middleware for service method calls without having to change our service. Here we just logged the runtime of a service method to the console but you can imagine that hooks can be useful for many other things like more advanced logging, sending notifications or checking user permissions.
You may also have noticed above that there are already some hooks like schemaHooks.validateQuery
or schemaHooks.resolveResult
registered on our service. This brings us to the next chapter on how to define our data model with schemas and resolvers.