Skip to main content

Understanding Service Paths

In Feathers, every service is registered at a specific path that becomes its API endpoint. The path determines how clients access the service through REST, Socket.io, or other transports.

Basic Path Registration

Basic Paths
import { feathers } from '@feathersjs/feathers'

const app = feathers()

// Service available at /users
app.use('/users', userService)

// Service available at /messages  
app.use('/messages', messageService)

// Service available at /api/posts
app.use('/api/posts', postService)
The path you register becomes the base URL for all service method calls:
  • GET /usersusers.find()
  • GET /users/123users.get(123)
  • POST /usersusers.create(data)
  • PATCH /users/123users.patch(123, data)
  • DELETE /users/123users.remove(123)

Path Normalization

Feathers automatically normalizes service paths by stripping leading and trailing slashes:
Path Normalization
// All of these register the service at 'users'
app.use('users', userService)
app.use('/users', userService)
app.use('/users/', userService)
app.use('users/', userService)

// All of these access the same service
app.service('users')
app.service('/users')
app.service('/users/')
app.service('users/')
Leading and trailing slashes are always stripped, so /users/ and users refer to the same service.

Root-Level Services

You can register a service at the root path:
Root Service
app.use('/', rootService)

// Access the root service
const result = await app.service('/').get('status')

// REST endpoints:
// GET / → find()
// GET /:id → get(id)
// POST / → create(data)

Nested Service Paths

Services can be registered at nested paths to create organized API structures:
Nested Paths
app.use('/api/v1/users', userService)
app.use('/api/v1/messages', messageService)
app.use('/api/v2/users', userServiceV2)

// Access nested services
const users = await app.service('api/v1/users').find()
const v2Users = await app.service('api/v2/users').find()

// REST endpoints:
// GET /api/v1/users
// POST /api/v1/messages
// GET /api/v2/users

Route Parameters

Feathers routing supports dynamic route parameters using the :param syntax:
Route Parameters
import { Router } from '@feathersjs/transport-commons'

const router = new Router()

// Register routes with parameters
router.insert('/users/:userId/posts', { service: 'posts' })
router.insert('/teams/:teamId/members/:memberId', { service: 'members' })

// Look up routes
const result = router.lookup('/users/123/posts')
console.log(result.data)     // { service: 'posts' }
console.log(result.params)   // { userId: '123' }

const result2 = router.lookup('/teams/456/members/789')
console.log(result2.params)  // { teamId: '456', memberId: '789' }

Accessing Route Parameters

Route parameters are available in the params.route object within service methods and hooks:
Using Route Parameters
class UserPostsService {
  async find(params) {
    const userId = params.route.userId
    
    // Find posts for specific user
    return this.posts.filter(post => post.userId === userId)
  }
  
  async create(data, params) {
    const userId = params.route.userId
    
    return {
      ...data,
      userId,
      createdAt: new Date()
    }
  }
}

// Register with route parameters
app.use('/users/:userId/posts', new UserPostsService())

// Use the service
const posts = await app.service('users/123/posts').find()

Sub-Applications

Mount entire Feathers applications as sub-applications to create modular, versioned APIs:
Sub-Applications
import { feathers } from '@feathersjs/feathers'

// Create main app
const app = feathers()

// Create a sub-application for API v1
const apiV1 = feathers()
apiV1.use('/users', userServiceV1)
apiV1.use('/posts', postServiceV1)
apiV1.use('/comments', commentServiceV1)

// Create a sub-application for API v2
const apiV2 = feathers()
apiV2.use('/users', userServiceV2)
apiV2.use('/posts', postServiceV2)

// Mount sub-apps with prefix
app.use('/api/v1', apiV1)
app.use('/api/v2', apiV2)

// Services are now available at:
// - /api/v1/users
// - /api/v1/posts
// - /api/v1/comments
// - /api/v2/users
// - /api/v2/posts
1

Create Sub-Application

Create a new Feathers application instance:
const subApp = feathers()
2

Register Services

Add services to the sub-application:
subApp.use('/users', userService)
subApp.use('/posts', postService)
3

Mount Sub-App

Mount the sub-application on the main app with a path prefix:
app.use('/api/v1', subApp)

Sub-Application Benefits

  • Modular Organization: Group related services together
  • API Versioning: Maintain multiple API versions side-by-side
  • Independent Configuration: Each sub-app can have its own hooks and settings
  • Service Reuse: Share services across multiple apps
Sub-App with Hooks
const adminApp = feathers()

// Add admin-specific authentication
adminApp.hooks({
  before: {
    all: [
      async (context) => {
        if (!context.params.user?.isAdmin) {
          throw new Error('Admin access required')
        }
      }
    ]
  }
})

adminApp.use('/users', adminUserService)
adminApp.use('/settings', settingsService)

// Mount admin app
app.use('/admin', adminApp)

// All services under /admin require admin access

Express Middleware and Service Paths

When using Feathers with Express, you can add middleware before and after services:
Middleware with Services
import express, { json, urlencoded } from '@feathersjs/express'
import { feathers } from '@feathersjs/feathers'

const app = express(feathers())

// Global middleware
app.use(json())
app.use(urlencoded({ extended: true }))

// Middleware before service
const checkApiKey = (req, res, next) => {
  if (!req.headers['x-api-key']) {
    return res.status(401).json({ error: 'API key required' })
  }
  next()
}

// Service with before/after middleware
app.use('/api/users',
  checkApiKey,           // Runs before service
  userService,
  (req, res, next) => {  // Runs after service
    res.set('X-Custom-Header', 'value')
    next()
  }
)

Middleware Options

Pass middleware as service options for better organization:
Middleware Options
app.use('/messages', messageService, {
  express: {
    before: [
      checkAuth,
      rateLimiter
    ],
    after: [
      addResponseHeaders
    ]
  }
})

Router Implementation

Feathers uses an efficient routing system based on a tree structure that supports parameter matching:
Custom Router
import { Router, RouteNode } from '@feathersjs/transport-commons'

// Create a router
const router = new Router()

// Case-sensitive routing (default)
router.caseSensitive = true

// Insert routes
router.insert('/users', { handler: 'userService' })
router.insert('/users/:id/posts', { handler: 'userPostsService' })
router.insert('/teams/:teamId/members/:memberId', { 
  handler: 'teamMemberService' 
})

// Look up routes
const match = router.lookup('/users/123/posts')
if (match) {
  console.log(match.data)    // { handler: 'userPostsService' }
  console.log(match.params)  // { id: '123' }
}

// Remove routes
router.remove('/users/:id/posts')

Route Matching Priority

  1. Exact matches are checked first
  2. Placeholder matches (:param) are checked if no exact match
  3. First matching placeholder wins if multiple placeholders exist
Route Priority
router.insert('/users/admin', { type: 'admin' })
router.insert('/users/:id', { type: 'user' })

// Exact match takes priority
router.lookup('/users/admin')  // { type: 'admin' }

// Falls back to placeholder
router.lookup('/users/123')    // { type: 'user' }, params: { id: '123' }

Case Sensitivity

By default, Feathers routing is case-sensitive:
Case Sensitivity
app.use('/Users', userService)      // /Users (case-sensitive)
app.use('/USERS', adminUserService) // /USERS (different service)

app.service('users')   // Not found
app.service('Users')   // Found
app.service('USERS')   // Found (different service)
To make routing case-insensitive:
Case-Insensitive Routing
const router = new Router()
router.caseSensitive = false

router.insert('/users', { service: 'users' })

// All of these match the same route
router.lookup('/users')
router.lookup('/Users')
router.lookup('/USERS')

Service Path Patterns

Common patterns for organizing service paths:
// Simple, flat API
app.use('/users', userService)
app.use('/posts', postService)
app.use('/comments', commentService)

// URLs:
// /users
// /posts
// /comments

Complete Routing Example

Here’s a complete example with nested routes, parameters, and middleware:
Complete Routing Example
import express from '@feathersjs/express'
import { feathers } from '@feathersjs/feathers'

const app = express(feathers())

// Middleware
const authenticate = (req, res, next) => {
  // Authentication logic
  next()
}

const checkTeamAccess = async (context) => {
  const teamId = context.params.route.teamId
  // Check if user has access to team
}

// Team members service with route parameters
class TeamMembersService {
  async find(params) {
    const { teamId } = params.route
    return this.getTeamMembers(teamId)
  }
  
  async create(data, params) {
    const { teamId } = params.route
    return {
      ...data,
      teamId,
      joinedAt: new Date()
    }
  }
}

// Register services with different patterns
app.use('/users', userService)
app.use('/teams', teamService)
app.use('/teams/:teamId/members', 
  authenticate,
  new TeamMembersService()
)

// Add authorization hook
app.service('teams/:teamId/members').hooks({
  before: {
    all: [checkTeamAccess]
  }
})

// API v2 sub-application
const apiV2 = feathers()
apiV2.use('/users', userServiceV2)
app.use('/api/v2', apiV2)

app.listen(3030)

Build docs developers (and LLMs) love