Skip to main content
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:
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)]
})

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:
1

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
  })
})
2

Apply to Results

Virtual properties are typically resolved on results:
import { resolveResult } from '@feathersjs/schema'

app.service('messages').hooks({
  around: {
    all: [resolveResult(messageResolver)]
  }
})
3

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:
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]'
  }
})
Handle paginated results with virtual properties:
const messageResolver = resolve<Message, HookContext>({
  userPage: virtual(async (message, context) => {
    // Returns paginated result
    return await context.app.service('users').find({
      query: {
        organizationId: message.organizationId,
        $limit: 10
      }
    })
  })
})

// resolveResult handles both single and paginated results
app.service('messages').hooks({
  around: {
    all: [resolveResult(messageResolver)]
  }
})
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