Remix
Remix

node-serve

Build high-performance Node.js servers with web-standard Fetch API primitives. Use this package when you want Remix-style Request/Response handlers with a managed server optimized for production throughput.

Features

  • Fetch API Handlers: Serve standard Request to Response request handlers
  • High-Performance Node.js Server: Start a fast managed server for Fetch API application code
  • HTTPS Support: Start a TLS server with certificate and key file paths
  • Managed Server Lifecycle: Start a server with serve(), wait for server.ready, and close it with server.close()
  • Custom Hostname: Override the host and protocol used to construct incoming request.url values
  • Client Info: Access client IP address, address family, and remote port when your handler accepts a second argument
  • uWebSockets.js Setup: Register native WebSocket routes and connection filters before the Fetch fallback starts listening
  • Existing uWebSockets.js App Adapter: Use createUwsRequestHandler() when you already own a uWebSockets.js app

Installation

npm i remix

node-serve includes uWebSockets.js as its native high-performance transport.

Usage

Use serve() to start a Node.js server that calls your fetch handler for every incoming request:

import { serve } from 'remix/node-serve'

let users = new Map([
  ['1', { id: '1', name: 'Alice', email: '[email protected]' }],
  ['2', { id: '2', name: 'Bob', email: '[email protected]' }],
])

async function handler(request: Request) {
  let url = new URL(request.url)

  if (url.pathname === '/' && request.method === 'GET') {
    return new Response('Welcome to the User API! Try GET /api/users')
  }

  if (url.pathname === '/api/users' && request.method === 'GET') {
    return Response.json(Array.from(users.values()))
  }

  let userMatch = url.pathname.match(/^\/api\/users\/(\w+)$/)
  if (userMatch && request.method === 'GET') {
    let user = users.get(userMatch[1])
    if (user) return Response.json(user)
    return new Response('User not found', { status: 404 })
  }

  return new Response('Not Found', { status: 404 })
}

let server = serve(handler, { port: 3000 })

await server.ready
console.log(`Server running at http://localhost:${server.port}`)

uWebSockets.js Setup

Use setup(app) when serve() should still manage the server lifecycle and Fetch fallback, but you need to configure native uWebSockets.js transport features before the server starts listening. The hook runs after the app is created and before node-serve registers its catch-all Fetch route.

This example adds a native WebSocket endpoint next to a normal Fetch handler on the same port:

import { serve } from 'remix/node-serve'

let server = serve(
  (request) => {
    let url = new URL(request.url)

    if (url.pathname === '/') {
      return new Response('Chat server')
    }

    return new Response('Not Found', { status: 404 })
  },
  {
    port: 3000,
    setup(app) {
      app.ws('/ws/chat', {
        open(ws) {
          ws.subscribe('chat')
        },
        message(ws, message, isBinary) {
          ws.publish('chat', message, isBinary)
        },
      })
    },
  },
)

await server.ready

app.ws(pattern, ...) uses uWebSockets.js route patterns, not Remix route-pattern syntax. uWS supports simple path params such as /rooms/:room, and those params are available during upgrade through req.getParameter('room') or req.getParameter(0). If WebSocket handlers need params, copy them into uWS user data during upgrade, then read them with ws.getUserData():

serve(handler, {
  setup(app) {
    app.ws<{ room: string }>('/rooms/:room', {
      upgrade(res, req, context) {
        res.upgrade(
          { room: req.getParameter('room') ?? 'general' },
          req.getHeader('sec-websocket-key'),
          req.getHeader('sec-websocket-protocol'),
          req.getHeader('sec-websocket-extensions'),
          context,
        )
      },
      open(ws) {
        ws.subscribe(`room:${ws.getUserData().room}`)
      },
    })
  },
})

uWS patterns do not support the full route-pattern feature set, including partial-segment params like v:version, split params like :file.:ext, optionals like /api(/v:version), host/protocol/search matching, or named multi-segment wildcard captures like *path. You can use a uWS pattern like /files/* as a catch-all, but it does not provide Remix-style named wildcard params.

You can also register connection filters for low-level transport metrics or limits:

let activeConnections = 0

serve(handler, {
  setup(app) {
    app.filter((_res, count) => {
      activeConnections = Number(count)
    })
  },
})

app.filter() observes low-level connection count changes. It is not Fetch response middleware and should not be used to mutate normal Response headers or bodies. Use Fetch handlers, middleware, or fetch-router for normal HTTP routing and response behavior; reserve setup() for uWebSockets.js transport features that must be configured before listening.

Custom Request URLs

Use host and protocol when your server runs behind a proxy or load balancer and you need stable incoming request URLs:

import { serve } from 'remix/node-serve'

let server = serve(handler, {
  host: process.env.HOST ?? 'api.example.com',
  protocol: 'https:',
  port: 3000,
})

await server.ready

HTTPS

Pass tls options to start an HTTPS server. keyFile and certFile are file paths, not PEM contents:

import { serve } from 'remix/node-serve'

let server = serve(handler, {
  port: 443,
  tls: {
    keyFile: './certs/server.key',
    certFile: './certs/server.crt',
  },
})

await server.ready
console.log(`Server running at https://localhost:${server.port}`)

When tls is present, request.url defaults to the https: protocol. You can still set protocol explicitly when the public URL differs from the local server transport.

Client Information

Handlers that accept a second argument receive the remote client address:

import { type FetchHandler, serve } from 'remix/node-serve'

let handler: FetchHandler = async (request, client) => {
  console.log(`Request from ${client.address}:${client.port}`)

  return Response.json({
    path: new URL(request.url).pathname,
    clientAddress: client.address,
  })
}

serve(handler, { port: 3000 })

Existing uWebSockets.js Apps

Most apps should use serve(). Use createUwsRequestHandler() when you already have a uWebSockets.js app and want only part of the app to use a Fetch API handler:

This example assumes uWebSockets.js is also a direct dependency of your app.

import { App } from 'uWebSockets.js'
import { createUwsRequestHandler } from 'remix/node-serve'

let app = App()

async function handler(request: Request) {
  let url = new URL(request.url)
  return Response.json({ path: url.pathname })
}

app.get('/health', (res) => {
  res.end('ok')
})

app.any('/api/*', createUwsRequestHandler(handler))

app.listen(3000, (socket) => {
  if (!socket) throw new Error('Could not listen on port 3000')
})

For HTTPS with an existing uWebSockets.js app, create the SSL app yourself and pass protocol: 'https:' when you create the request handler:

import { SSLApp } from 'uWebSockets.js'
import { createUwsRequestHandler } from 'remix/node-serve'

let app = SSLApp({
  key_file_name: './certs/server.key',
  cert_file_name: './certs/server.crt',
})

app.any('/*', createUwsRequestHandler(handler, { protocol: 'https:' }))
app.listen(443, (socket) => {
  if (!socket) throw new Error('Could not listen on port 443')
})

Related Packages

  • node-fetch-server - Adapt Fetch API handlers to existing node:http, node:https, and node:http2 servers
  • fetch-router - Route incoming Request objects to Fetch API handlers

Related Work

  • Fetch API - Web standard Request and Response primitives

License

See LICENSE