Documentation Index Fetch the complete documentation index at: https://mintlify.com/feathersjs/feathers/llms.txt
Use this file to discover all available pages before exploring further.
Resolvers transform data properties before or after service method execution. They enable powerful patterns like hashing passwords, computing virtual properties, and controlling data visibility.
Resolver Basics
Resolvers are defined as property maps where each property has a resolver function:
import { resolve } from '@feathersjs/schema'
import { HookContext } from '@feathersjs/feathers'
type User = {
firstName : string
lastName : string
password : string
name ?: string
}
const userResolver = resolve < User , HookContext >({
// Hash password before saving
password : async ( value ) => {
return await bcrypt . hash ( value , 10 )
},
// Compute full name from other properties
name : async ( value , user ) => {
return ` ${ user . firstName } ${ user . lastName } `
}
})
Resolver Hooks
The schema system provides hooks for different resolver scenarios:
resolveData
resolveResult
resolveQuery
resolveExternal
Transform data before it reaches the service method: import { resolveData } from '@feathersjs/schema'
app . service ( 'users' ). hooks ({
create: [ resolveData ( userDataResolver )],
update: [ resolveData ( userDataResolver )],
patch: [ resolveData ( userDataResolver )]
})
Transform result after the service method (around hook): import { resolveResult } from '@feathersjs/schema'
app . service ( 'users' ). hooks ({
around: {
all: [ resolveResult ( userResultResolver )]
}
})
Transform query parameters before the service method: import { resolveQuery } from '@feathersjs/schema'
app . service ( 'messages' ). hooks ({
find: [ resolveQuery ( messageQueryResolver )],
get: [ resolveQuery ( messageQueryResolver )]
})
Transform data for external clients (around hook): import { resolveExternal } from '@feathersjs/schema'
app . service ( 'users' ). hooks ({
around: {
all: [ resolveExternal ( userExternalResolver )]
}
})
Property Resolvers
Property resolvers receive four parameters:
import { resolve , PropertyResolver } from '@feathersjs/schema'
import { HookContext } from '@feathersjs/feathers'
const resolver = resolve < User , HookContext >({
name : async (
value , // Current value of the property
obj , // The entire data object
context , // The hook context
status // Resolver status (path, stack, etc.)
) => {
console . log ( 'Resolver path:' , status . path ) // ['name']
console . log ( 'User email:' , obj . email )
return ` ${ obj . firstName } ${ obj . lastName } `
}
})
Virtual Properties
Virtual properties are computed values that don’t exist in the original data:
Define Virtual Property
Use the virtual helper to create computed properties: import { resolve , virtual } from '@feathersjs/schema'
import { HookContext } from '@feathersjs/feathers'
type Message = {
text : string
userId : number
user ?: User // Virtual property
}
const messageResolver = resolve < Message , HookContext >({
user: virtual ( async ( message , context ) => {
// Fetch related user
const user = await context . app . service ( 'users' ). get ( message . userId )
return user
})
})
Apply to Results
Virtual properties are typically resolved on results: import { resolveResult } from '@feathersjs/schema'
app . service ( 'messages' ). hooks ({
around: {
all: [ resolveResult ( messageResolver )]
}
})
Query with $resolve
Use $resolve to select specific virtual properties: // Only resolve the 'user' virtual property
await app . service ( 'messages' ). find ({
query: {
$resolve: [ 'user' ]
}
})
Virtual Properties with $select
Virtual properties work seamlessly with $select:
const messageResolver = resolve < Message , HookContext >({
user: virtual ( async ( message , context ) => {
return await context . app . service ( 'users' ). get ( message . userId )
}),
userList: virtual ( async ( message , context ) => {
return await context . app . service ( 'users' ). find ({
query: { organizationId: message . organizationId }
})
})
})
app . service ( 'messages' ). hooks ({
around: {
all: [ resolveResult ( messageResolver )]
}
})
// Select only specific virtual properties
await app . service ( 'messages' ). find ({
query: {
$select: [ 'text' , 'user' ] // Resolves 'user' but not 'userList'
}
})
Secure Data Handling
Use resolvers to protect sensitive data:
Hash Passwords
Hide Sensitive Fields
Role-Based Visibility
const userDataResolver = resolve < UserData , HookContext >({
password : async ( value ) => {
// Always hash passwords before saving
return await bcrypt . hash ( value , 10 )
}
})
app . service ( 'users' ). hooks ({
create: [ resolveData ( userDataResolver )],
update: [ resolveData ( userDataResolver )],
patch: [ resolveData ( userDataResolver )]
})
Multiple Resolvers
Chain multiple resolvers for complex transformations:
import { resolveResult } from '@feathersjs/schema'
const firstResolver = resolve < User , HookContext >({
name : async ( value , user ) => user . email . split ( '@' )[ 0 ]
})
const secondResolver = resolve < User , HookContext >({
name : async ( value , user ) => ` ${ value } ( ${ user . email } )`
})
// Resolvers are applied in order
app . service ( 'users' ). hooks ({
around: {
all: [ resolveResult ( firstResolver , secondResolver )]
}
})
// Input: { email: 'dave@example.com' }
// After first: { email: '...', name: 'dave' }
// After second: { email: '...', name: 'dave (dave@example.com)' }
Converters
Converters transform the entire data object before property resolution:
import { resolve } from '@feathersjs/schema'
const userResolver = resolve < User , HookContext >({
converter : async ( data , context ) => {
// Transform or add defaults
return {
firstName: 'Guest' ,
lastName: 'User' ,
... data ,
updatedAt: new Date ()
}
},
properties: {
name : async ( value , user ) => ` ${ user . firstName } ${ user . lastName } `
}
})
// Converter runs before property resolvers
await userResolver . resolve ({}, context )
// Result: { firstName: 'Guest', lastName: 'User', name: 'Guest User', updatedAt: Date }
Query Resolvers
Transform query parameters for security and convenience:
import { resolveQuery } from '@feathersjs/schema'
import { HookContext } from '@feathersjs/feathers'
type MessageQuery = {
text ?: string
userId ?: number
}
const messageQueryResolver = resolve < MessageQuery , HookContext >({
userId : async ( value , query , context ) => {
// Automatically filter by authenticated user
if ( context . params . user ) {
return context . params . user . id
}
return value
}
})
app . service ( 'messages' ). hooks ({
find: [ resolveQuery ( messageQueryResolver )]
})
// User only sees their own messages
await app . service ( 'messages' ). find ()
// Query is automatically modified to: { userId: <current-user-id> }
Resolver Status
Resolvers receive a status object with metadata:
import { resolve , ResolverStatus } from '@feathersjs/schema'
const resolver = resolve < User , HookContext >({
name : async ( value , user , context , status ) => {
console . log ( 'Current path:' , status . path ) // ['name']
console . log ( 'Property stack:' , status . stack ) // Array of resolvers
console . log ( 'Original context:' , status . originalContext )
console . log ( 'Selected properties:' , status . properties )
return ` ${ user . firstName } ${ user . lastName } `
}
})
Error Handling
Resolvers can throw errors that are automatically collected:
import { resolve } from '@feathersjs/schema'
import { BadRequest } from '@feathersjs/errors'
const userResolver = resolve < User , HookContext >({
name : async ( value ) => {
if ( value === 'Admin' ) {
throw new Error ( 'Reserved name' )
}
return value
},
age : async ( value ) => {
if ( value && value < 18 ) {
throw new BadRequest ( 'Must be 18 or older' )
}
return value
}
})
try {
await userResolver . resolve ({ name: 'Admin' , age: 16 }, context )
} catch ( error ) {
console . log ( error . name ) // 'BadRequest'
console . log ( error . message ) // 'Error resolving data'
console . log ( error . data )
// {
// name: { message: 'Reserved name' },
// age: {
// name: 'BadRequest',
// message: 'Must be 18 or older',
// code: 400,
// className: 'bad-request'
// }
// }
}
Circular Dependency Prevention
Resolvers automatically prevent circular dependencies:
const resolver = resolve < User , HookContext >({
fullName : async ( value , user , context , status ) => {
// If already in the stack, returns undefined to prevent infinite loop
if ( status . stack . includes ( resolver )) {
return undefined
}
return ` ${ user . firstName } ${ user . lastName } `
}
})
Advanced Patterns
Resolve nested objects and arrays: const messageResolver = resolve < Message , HookContext >({
user: virtual ( async ( message , context ) => {
const user = await context . app . service ( 'users' ). get ( message . userId )
return user
}),
comments: virtual ( async ( message , context ) => {
const comments = await context . app . service ( 'comments' ). find ({
query: { messageId: message . id }
})
return comments . data
})
})
Resolve properties based on conditions: const userResolver = resolve < User , HookContext >({
email : async ( value , user , context ) => {
// Only show email to owner or admin
const currentUser = context . params . user
if ( currentUser ?. id === user . id || currentUser ?. isAdmin ) {
return value
}
return '[hidden]'
}
})
Pagination with Virtual Properties
External vs Internal Resolution
Use different resolvers for internal and external data: // Internal resolver - adds computed properties
const messageResultResolver = resolve < Message , HookContext >({
user: virtual ( async ( message , context ) => {
return await context . app . service ( 'users' ). get ( message . userId )
})
})
// External resolver - hides sensitive data
const messageExternalResolver = resolve < Message , HookContext >({
userId : async () => undefined , // Hide user ID
user : async ( value : User ) => ({
// Only expose public user data
id: value . id ,
name: value . name
})
})
app . service ( 'messages' ). hooks ({
around: {
all: [
resolveResult ( messageResultResolver ),
resolveExternal ( messageExternalResolver )
]
}
})
Resolver with Schema Validation
Combine resolvers with schema validation:
import { resolve , schema } from '@feathersjs/schema'
import { validateData , resolveData } from '@feathersjs/schema'
const userDataSchema = schema ({
$id: 'UserData' ,
type: 'object' ,
required: [ 'email' , 'password' ],
properties: {
email: { type: 'string' },
password: { type: 'string' }
}
} as const )
const userDataResolver = resolve < UserData , HookContext >({
schema: userDataSchema ,
validate: 'before' , // Validate before resolving
properties: {
password : async ( value ) => await bcrypt . hash ( value , 10 )
}
})
// Or use separate hooks (recommended)
app . service ( 'users' ). hooks ({
create: [
validateData ( userDataSchema ),
resolveData ( userDataResolver )
]
})
Best Practices
Use virtual for computed properties Always use virtual() for properties that don’t exist in the original data.
Resolve queries for security Use query resolvers to enforce access control and prevent data leaks.
Separate internal and external Use resolveResult for internal data and resolveExternal for client data.
Chain resolvers for clarity Use multiple focused resolvers instead of one complex resolver.
Next Steps
Data Validation Learn how to validate data with schemas
Schema Overview Back to schema system overview