HTTP

The feathers/http module provides a Web Standard HTTP handler that works across all JavaScript runtimes: Node.js, Deno, Bun, and Cloudflare Workers.

createHandler

createHandler(app, middleware?) creates a Web Standard request handler that processes HTTP requests for your Feathers application.

import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'

const app = feathers()

app.use('messages', {
  async find() {
    return [{ id: 1, text: 'Hello world' }]
  }
})

const handler = createHandler(app)

The handler has the signature (request: Request) => Promise<Response> which is the Web Standard used by Deno, Bun, and Cloudflare Workers.

Options

  • app - The Feathers application
  • middleware - Optional array of middleware. Defaults to [errorHandler(), queryParser(), bodyParser()]

Runtime Usage

Deno

import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'

const app = feathers()
// ... configure your app

const handler = createHandler(app)

Deno.serve({ port: 3030 }, handler)

Bun

import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'

const app = feathers()
// ... configure your app

const handler = createHandler(app)

Bun.serve({
  port: 3030,
  fetch: handler
})

Cloudflare Workers

import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'

const app = feathers()
// ... configure your app

const handler = createHandler(app)

export default {
  fetch: handler
}

Node.js

Node.js does not have a built-in Web Standard HTTP server, so an adapter is required. The toNodeHandler function converts the Web Standard handler to work with Node's http.createServer.

import { createServer } from 'node:http'
import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'
import { toNodeHandler } from 'feathers/http/node'

const app = feathers()
// ... configure your app

const handler = createHandler(app)
const server = createServer(toNodeHandler(handler))

server.listen(3030, () => {
  console.log('Server running on http://localhost:3030')
})

// Call app.setup to initialize all services
await app.setup(server)

toNodeHandler

toNodeHandler(handler) converts a Web Standard (Request) => Promise<Response> handler to Node's (IncomingMessage, ServerResponse) => void signature.

import { toNodeHandler } from 'feathers/http/node'

const nodeHandler = toNodeHandler(handler)

This adapter:

  • Converts Node's IncomingMessage to a Web Standard Request
  • Buffers JSON, form-urlencoded, and multipart requests for proper parsing
  • Streams all other content types directly
  • Writes the Web Standard Response back to Node's ServerResponse

Middleware

The HTTP handler uses a middleware chain for request processing. The default middleware handles common tasks like error handling, query parsing, and body parsing.

errorHandler

Catches errors and returns them as properly formatted JSON responses with appropriate status codes.

import { errorHandler } from 'feathers/http'

queryParser

Parses URL query parameters using qs and adds them to params.query.

import { queryParser } from 'feathers/http'

// With custom parser
import qs from 'qs'
queryParser((query) => qs.parse(query, { arrayLimit: 200 }))

bodyParser

Parses request bodies based on content type:

Content-TypeParsing Method
application/jsonrequest.json()
application/x-www-form-urlencodedrequest.text()URLSearchParams
multipart/form-datarequest.formData()
Everything elseStreams request.body directly
import { bodyParser } from 'feathers/http'

Custom Middleware

You can provide custom middleware to the handler:

import { createHandler, errorHandler, queryParser, bodyParser } from 'feathers/http'

const customLogger = async (context, next) => {
  console.log(`${context.request.method} ${context.request.url}`)
  await next()
}

const handler = createHandler(app, [errorHandler(), customLogger, queryParser(), bodyParser()])

params

params.query

Contains the URL query parameters parsed by the queryParser middleware.

// GET /messages?status=read&limit=10
// params.query = { status: 'read', limit: '10' }

params.provider

For any service method call made through HTTP, params.provider will be set to 'rest'.

params.headers

Contains the request headers as a plain object.

// params.headers = { 'content-type': 'application/json', ... }

params.route

Route placeholders in a service URL will be added to params.route.

app.use('users/:userId/messages', messageService)

// GET /users/123/messages
// params.route = { userId: '123' }

params.request

The original Web Standard Request object is available as params.request.

Content Types

JSON

Standard JSON requests and responses:

// Request
POST /messages
Content-Type: application/json

{ "text": "Hello world" }

// Service receives
data = { text: 'Hello world' }

Form Data

URL-encoded and multipart form data are automatically parsed:

// Request
POST /messages
Content-Type: application/x-www-form-urlencoded

text=Hello+world&status=sent

// Service receives
data = { text: 'Hello world', status: 'sent' }

File Uploads

Multipart file uploads use the Web Standard FormData API:

// Request
POST /uploads
Content-Type: multipart/form-data

// Service receives
data = {
  file: File,           // Web Standard File object
  description: 'string'
}

Multiple files with the same field name become an array:

data = {
  files: [File, File, File]
}

Streaming

Non-buffered content types are streamed directly to the service:

class UploadService {
  async create(stream: ReadableStream, params: Params) {
    // Stream data directly to storage
    const reader = stream.getReader()

    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      // Process chunks
    }

    return { uploaded: true }
  }
}

Returning Responses

Services can return a Web Standard Response directly for full control:

class DownloadService {
  async get(id: string) {
    const file = await storage.get(id)

    return new Response(file.stream, {
      headers: {
        'Content-Type': file.contentType,
        'Content-Disposition': `attachment; filename="${file.name}"`
      }
    })
  }
}

Async Iterators (SSE)

Services can return async iterators for Server-Sent Events:

class StreamService {
  async find() {
    return (async function* () {
      for (let i = 0; i < 10; i++) {
        yield { count: i }
        await new Promise((resolve) => setTimeout(resolve, 1000))
      }
    })()
  }
}

The response will be sent as text/event-stream:

data: {"count":0}

data: {"count":1}

data: {"count":2}
...

SSE Service

The SseService provides real-time event streaming to clients using Server-Sent Events. It works with channels to push created, updated, patched, removed and custom events to connected clients over a persistent HTTP connection.

Setup

Register the SseService on a path in your application:

import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'
import { SseService } from 'feathers/sse'

const app = feathers()

// Register your services
app.use('messages', new MessageService())

// Register the SSE service
app.use('sse', new SseService())

// Set up channels
app.on('connection', (connection) => {
  app.channel('anonymous').join(connection)
})

app.publish(() => app.channel('anonymous'))

const handler = createHandler(app)

How it works

When a client connects to the SSE service via find, the service:

  1. Registers the client's connection with the application (emitting the connection event)
  2. Opens a persistent SSE stream
  3. Sends a connected event to confirm the connection
  4. Listens for events published through channels and streams them to the client
  5. Automatically cleans up when the connection is closed

Each event sent to the client includes:

  • event - The event name (e.g. created, patched)
  • data - The event payload
  • path - The service path that emitted the event

With Node.js

import { createServer } from 'node:http'
import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'
import { toNodeHandler } from 'feathers/http/node'
import { SseService } from 'feathers/sse'

const app = feathers()

app.use('messages', new MessageService())
app.use('sse', new SseService())

app.on('connection', (connection) => {
  app.channel('authenticated').join(connection)
})

app.publish(() => app.channel('authenticated'))

const handler = createHandler(app)
const server = createServer(toNodeHandler(handler))

server.listen(3030)
await app.setup(server)

Client connection

On the client, enable SSE by passing the sse option to fetchClient. The SSE connection is automatically established during app.setup():

import { feathers } from 'feathers'
import { fetchClient } from 'feathers/client'

const app = feathers()

app.configure(fetchClient(fetch, {
  baseUrl: 'http://localhost:3030',
  sse: 'sse' // The path where SseService is registered
}))

await app.setup()

// Real-time events are now received automatically
app.service('messages').on('created', (message) => {
  console.log('New message', message)
})

For more details on the client-side SSE configuration, see the SSE client documentation.

CORS

The handler automatically sets CORS headers based on the request's Origin header:

Access-Control-Allow-Origin: <request origin or *>

For preflight OPTIONS requests, the handler returns:

Access-Control-Allow-Origin: <origin>
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: accept, accept-language, content-language, content-type, range, authorization, x-service-method
Access-Control-Allow-Credentials: true

Custom Methods

Custom service methods can be called via HTTP by setting the X-Service-Method header:

POST /messages
X-Service-Method: myCustomMethod
Content-Type: application/json

{ "data": "value" }

This will call messages.myCustomMethod({ data: 'value' }, params).

Subscribe to our NewsletterGet new Feathers content as it becomes available.
FeathersJSFeathersJS

Copyright © 2012 - 2026 feathers.dev