Skip to main content
This guide covers patterns and techniques that apply to all Feathers database adapters, helping you build robust and maintainable data services.

Service Patterns

Extending Adapters

Custom service classes allow you to add business logic:
import { MongoDBService } from '@feathersjs/mongodb'
import type { Params } from '@feathersjs/feathers'

interface Article {
  _id: ObjectId
  title: string
  content: string
  createdAt: Date
  updatedAt: Date
}

class ArticleService extends MongoDBService<Article> {
  async create(data: Partial<Article>, params?: Params) {
    const articleData = {
      ...data,
      createdAt: new Date(),
      updatedAt: new Date()
    }
    return super.create(articleData, params)
  }

  async patch(id: any, data: Partial<Article>, params?: Params) {
    const patchData = {
      ...data,
      updatedAt: new Date()
    }
    return super.patch(id, patchData, params)
  }
}

Multi-tenancy

Implement multi-tenancy with query filtering:
import { MongoDBService } from '@feathersjs/mongodb'

class TenantService extends MongoDBService {
  async find(params?: Params) {
    const tenantId = params?.user?.tenantId
    if (!tenantId) {
      throw new Forbidden('No tenant context')
    }
    
    return super.find({
      ...params,
      query: {
        ...params?.query,
        tenantId
      }
    })
  }

  async create(data: any, params?: Params) {
    const tenantId = params?.user?.tenantId
    if (!tenantId) {
      throw new Forbidden('No tenant context')
    }
    
    return super.create({
      ...data,
      tenantId
    }, params)
  }

  async get(id: any, params?: Params) {
    const tenantId = params?.user?.tenantId
    return super.get(id, {
      ...params,
      query: {
        ...params?.query,
        tenantId
      }
    })
  }
}

Query Patterns

Complex Queries

Build sophisticated queries using operators:
// Find records in date range
const startDate = new Date('2024-01-01')
const endDate = new Date('2024-12-31')

const results = await app.service('orders').find({
  query: {
    createdAt: {
      $gte: startDate,
      $lte: endDate
    },
    status: 'completed'
  }
})

Pagination Patterns

// Efficient pagination for large datasets
interface CursorParams extends Params {
  query?: {
    cursor?: string
    $limit?: number
  }
}

class CursorService extends MongoDBService {
  async find(params?: CursorParams) {
    const limit = params?.query?.$limit || 20
    const cursor = params?.query?.cursor
    
    const query: any = {}
    
    if (cursor) {
      // Decode cursor (in practice, use proper encoding)
      query._id = { $gt: new ObjectId(cursor) }
    }
    
    const results = await super.find({
      ...params,
      query: {
        ...params?.query,
        ...query,
        $limit: limit,
        $sort: { _id: 1 }
      },
      paginate: false
    })
    
    const hasMore = results.length === limit
    const nextCursor = hasMore
      ? results[results.length - 1]._id.toString()
      : null
    
    return {
      data: results,
      nextCursor,
      hasMore
    }
  }
}

Performance Optimization

Query Optimization

// Only fetch needed fields
const users = await app.service('users').find({
  query: {
    status: 'active',
    $select: ['id', 'name', 'email']
    // Exclude large fields like 'bio', 'avatar', etc.
  }
})

Caching Strategies

import { HookContext } from '@feathersjs/feathers'

const cache = new Map()

const cacheResults = async (context: HookContext) => {
  const cacheKey = JSON.stringify(context.params.query)
  
  if (context.method === 'find') {
    const cached = cache.get(cacheKey)
    if (cached && Date.now() - cached.timestamp < 60000) {
      context.result = cached.data
      return context
    }
  }
  
  return context
}

const saveToCache = async (context: HookContext) => {
  if (context.method === 'find') {
    const cacheKey = JSON.stringify(context.params.query)
    cache.set(cacheKey, {
      data: context.result,
      timestamp: Date.now()
    })
  }
  return context
}

// Invalidate cache on mutations
const invalidateCache = async (context: HookContext) => {
  cache.clear()
  return context
}

app.service('users').hooks({
  before: {
    find: [cacheResults]
  },
  after: {
    find: [saveToCache],
    create: [invalidateCache],
    update: [invalidateCache],
    patch: [invalidateCache],
    remove: [invalidateCache]
  }
})

Data Validation

Schema Validation

import { hooks, querySyntax, Ajv } from '@feathersjs/schema'

const userSchema = {
  $id: 'User',
  type: 'object',
  additionalProperties: false,
  required: ['email', 'name'],
  properties: {
    id: { type: 'number' },
    email: { type: 'string', format: 'email' },
    name: { type: 'string', minLength: 2 },
    age: { type: 'number', minimum: 0, maximum: 150 }
  }
}

const userValidator = new Ajv().compile(userSchema)

app.service('users').hooks({
  before: {
    create: [hooks.validateData(userValidator)],
    update: [hooks.validateData(userValidator)],
    patch: [hooks.validateData(userValidator)]
  }
})

Error Handling

Adapter-specific Errors

import { Conflict, BadRequest } from '@feathersjs/errors'
import { HookContext } from '@feathersjs/feathers'

const handleMongoErrors = async (context: HookContext) => {
  const error = context.error as any
  
  if (error.code === 11000) {
    // Duplicate key error
    const field = Object.keys(error.keyPattern)[0]
    throw new Conflict(`Duplicate ${field}`, {
      field,
      value: error.keyValue[field]
    })
  }
  
  if (error.name === 'ValidationError') {
    throw new BadRequest('Validation failed', error.errors)
  }
  
  throw error
}

app.service('users').hooks({
  error: {
    all: [handleMongoErrors]
  }
})

Testing

Unit Testing Services

import { MemoryService } from '@feathersjs/memory'
import assert from 'assert'

describe('User Service', () => {
  let service: MemoryService
  
  beforeEach(() => {
    service = new MemoryService({
      paginate: {
        default: 10,
        max: 50
      }
    })
  })
  
  it('creates a user', async () => {
    const user = await service.create({
      name: 'Test User',
      email: 'test@example.com'
    })
    
    assert.strictEqual(user.name, 'Test User')
    assert.strictEqual(user.email, 'test@example.com')
    assert.ok(user.id)
  })
  
  it('finds users with pagination', async () => {
    await service.create({ name: 'User 1' })
    await service.create({ name: 'User 2' })
    
    const results = await service.find({
      query: { $limit: 1 }
    })
    
    assert.strictEqual(results.total, 2)
    assert.strictEqual(results.data.length, 1)
  })
})

Next Steps

MongoDB Adapter

Deep dive into MongoDB features

Knex Adapter

Learn SQL-specific patterns

Hooks

Master service hooks

Authentication

Secure your services