The feathers/http module provides a Web Standard HTTP handler that works across all JavaScript runtimes: Node.js, Deno, Bun, and Cloudflare Workers.
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.
app - The Feathers applicationmiddleware - Optional array of middleware. Defaults to [errorHandler(), queryParser(), bodyParser()]import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'
const app = feathers()
// ... configure your app
const handler = createHandler(app)
Deno.serve({ port: 3030 }, handler)
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
})
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 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(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:
IncomingMessage to a Web Standard RequestResponse back to Node's ServerResponseThe HTTP handler uses a middleware chain for request processing. The default middleware handles common tasks like error handling, query parsing, and body parsing.
Catches errors and returns them as properly formatted JSON responses with appropriate status codes.
import { errorHandler } from 'feathers/http'
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 }))
Parses request bodies based on content type:
| Content-Type | Parsing Method |
|---|---|
application/json | request.json() |
application/x-www-form-urlencoded | request.text() → URLSearchParams |
multipart/form-data | request.formData() |
| Everything else | Streams request.body directly |
import { bodyParser } from 'feathers/http'
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()])
Contains the URL query parameters parsed by the queryParser middleware.
// GET /messages?status=read&limit=10
// params.query = { status: 'read', limit: '10' }
For any service method call made through HTTP, params.provider will be set to 'rest'.
Contains the request headers as a plain object.
// params.headers = { 'content-type': 'application/json', ... }
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' }
The original Web Standard Request object is available as params.request.
Standard JSON requests and responses:
// Request
POST /messages
Content-Type: application/json
{ "text": "Hello world" }
// Service receives
data = { text: 'Hello world' }
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' }
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]
}
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 }
}
}
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}"`
}
})
}
}
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}
...
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.
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)
When a client connects to the SSE service via find, the service:
connection event)connected event to confirm the connectionEach event sent to the client includes:
event - The event name (e.g. created, patched)data - The event payloadpath - The service path that emitted the eventimport { 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)
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.
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 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).