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: '[email protected]'
})
assert.strictEqual(user.name, 'Test User')
assert.strictEqual(user.email, '[email protected]')
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