Skip to content

node-fetch-server ​

The node-fetch-server package bridges the gap between the Fetch API and Node.js. It converts a standard Fetch handler (a function that takes a Request and returns a Response) into a Node.js request listener that works with http.createServer(), https.createServer(), and http2.createServer().

This is how you run a Remix application on Node.js.

Installation ​

bash
# Via the meta-package
npm install remix

# Or individually
npm install @remix-run/node-fetch-server

Imports ​

ts
import {
  createRequestListener,
  createRequest,
  createHeaders,
  sendResponse,
} from 'remix/node-fetch-server'

// Types
import type {
  RequestListenerOptions,
  RequestOptions,
  FetchHandler,
  ErrorHandler,
  ClientAddress,
} from 'remix/node-fetch-server'

createRequestListener(handler, options?) ​

Wraps a Fetch handler in a Node.js request listener. This is the primary function you use to serve a Remix application.

ts
import * as http from 'node:http'
import { createRequestListener } from 'remix/node-fetch-server'

async function handler(request: Request): Promise<Response> {
  return new Response('Hello, world!')
}

let server = http.createServer(createRequestListener(handler))
server.listen(3000)

With a Remix Router ​

The most common usage is to pass a router's fetch method:

ts
import * as http from 'node:http'
import { createRouter } from 'remix/fetch-router'
import { createRequestListener } from 'remix/node-fetch-server'

let router = createRouter()

router.get('/', () => new Response('Home'))
router.get('/about', () => new Response('About'))

let server = http.createServer(createRequestListener(router.fetch))
server.listen(3000, () => {
  console.log('Listening on http://localhost:3000')
})

Parameters ​

ParameterTypeDescription
handlerFetchHandlerA function that takes a Request and ClientAddress and returns a Response.
optionsRequestListenerOptionsOptional configuration.

Returns ​

An http.RequestListener function that can be passed to http.createServer().


RequestListenerOptions ​

OptionTypeDefaultDescription
hoststringFrom Host headerOverrides the host portion of the request URL.
protocolstringFrom connectionOverrides the protocol of the request URL.
onErrorErrorHandlerLogs + 500Custom error handler for when the fetch handler throws.

Custom Host ​

Use the host option when your server is behind a reverse proxy and you want request URLs to reflect the public hostname:

ts
createRequestListener(handler, {
  host: process.env.HOST,  // e.g. "example.com"
})

Custom Protocol ​

Force all request URLs to use HTTPS (useful behind a TLS-terminating proxy):

ts
createRequestListener(handler, {
  protocol: 'https:',
})

Custom Error Handler ​

Handle errors thrown by the fetch handler:

ts
createRequestListener(handler, {
  onError(error) {
    console.error('Request failed:', error)
    return new Response('Something went wrong', { status: 500 })
  },
})

If the error handler itself throws or returns undefined, a default 500 Internal Server Error response is sent.


FetchHandler ​

The type for functions that handle Fetch API requests.

ts
interface FetchHandler {
  (request: Request, client: ClientAddress): Response | Promise<Response>
}

The client parameter provides information about the remote client that sent the request.


ClientAddress ​

Information about the client that sent a request.

ts
interface ClientAddress {
  address: string        // IP address (e.g. "127.0.0.1")
  family: 'IPv4' | 'IPv6'
  port: number           // Remote port
}

You can use the client address for logging, rate limiting, or IP-based access control:

ts
let handler: FetchHandler = (request, client) => {
  console.log(`Request from ${client.address}:${client.port}`)
  return new Response('OK')
}

ErrorHandler ​

A function that handles errors thrown during request processing.

ts
interface ErrorHandler {
  (error: unknown): void | Response | Promise<void | Response>
}

If it returns a Response, that response is sent to the client. If it returns void or undefined, a default 500 response is sent.


createRequest(req, res, options?) ​

Creates a Fetch API Request from a Node.js IncomingMessage and ServerResponse pair. This is a lower-level function --- createRequestListener() calls it internally.

ts
import * as http from 'node:http'
import { createRequest, sendResponse } from 'remix/node-fetch-server'

let server = http.createServer(async (req, res) => {
  let request = createRequest(req, res)

  // Use the standard Request object
  console.log(request.method, request.url)

  let response = new Response('Hello')
  await sendResponse(res, response)
})

Parameters ​

ParameterTypeDescription
reqhttp.IncomingMessage | http2.Http2ServerRequestThe Node.js incoming request.
reshttp.ServerResponse | http2.Http2ServerResponseThe Node.js server response.
optionsRequestOptionsOptional host and protocol overrides.

Streaming Request Bodies ​

For requests with bodies (POST, PUT, etc.), the request body is provided as a ReadableStream that streams data from the Node.js request. The body is not buffered in memory.

Abort Signals ​

The created Request has an AbortSignal that fires when the client disconnects (the response's close event fires without a preceding finish event). This allows handlers to stop work early when the client goes away.


createHeaders(req) ​

Creates a Fetch API Headers object from a Node.js incoming message. Handles multi-value headers correctly and filters out HTTP/2 pseudo-headers.

ts
import { createHeaders } from 'remix/node-fetch-server'

let headers = createHeaders(req)
console.log(headers.get('Content-Type'))

sendResponse(res, response) ​

Sends a Fetch API Response through a Node.js ServerResponse. Handles streaming response bodies, HTTP/2 compatibility, and multiple Set-Cookie headers correctly.

ts
import { sendResponse } from 'remix/node-fetch-server'

let response = new Response('Hello', {
  headers: { 'Content-Type': 'text/plain' },
})

await sendResponse(res, response)

Streaming ​

If the response has a body that is a ReadableStream, sendResponse streams it chunk by chunk to the client. It respects Node.js backpressure --- if the socket buffer is full, it waits for a drain event before writing more data.

HTTP/2 Support ​

sendResponse automatically detects HTTP/2 responses and omits the status text (which HTTP/2 does not support), avoiding Node.js warnings.


HTTPS Support ​

Use createRequestListener() with https.createServer() for HTTPS:

ts
import * as https from 'node:https'
import * as fs from 'node:fs'
import { createRequestListener } from 'remix/node-fetch-server'

let server = https.createServer(
  {
    key: fs.readFileSync('key.pem'),
    cert: fs.readFileSync('cert.pem'),
  },
  createRequestListener(handler)
)

server.listen(443)

The request URL protocol is automatically detected as https: when using https.createServer().


HTTP/2 Support ​

Works with both http2.createServer() and http2.createSecureServer():

ts
import * as http2 from 'node:http2'
import { createRequestListener } from 'remix/node-fetch-server'

let server = http2.createSecureServer(
  {
    key: fs.readFileSync('key.pem'),
    cert: fs.readFileSync('cert.pem'),
  },
  createRequestListener(handler)
)

server.listen(443)

Complete Example ​

ts
import * as http from 'node:http'
import { createRouter } from 'remix/fetch-router'
import { route, form } from 'remix/fetch-router/routes'
import { createRequestListener } from 'remix/node-fetch-server'
import { logger } from 'remix/logger-middleware'
import { compression } from 'remix/compression-middleware'

// Define routes
let routes = route({
  home: '/',
  contact: form('contact'),
  health: '/health',
})

// Create router with middleware
let router = createRouter({
  middleware: [logger(), compression()],
})

// Register handlers
router.get(routes.home, () => {
  return new Response('<h1>Welcome</h1>', {
    headers: { 'Content-Type': 'text/html' },
  })
})

router.get(routes.health, () => {
  return Response.json({ status: 'ok' })
})

// Start the server
let port = Number(process.env.PORT) || 3000
let server = http.createServer(
  createRequestListener(router.fetch, {
    onError(error) {
      console.error('Unhandled error:', error)
      return new Response('Internal Server Error', { status: 500 })
    },
  })
)

server.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}`)
})

  • fetch-router --- The router that produces the FetchHandler you pass to createRequestListener().
  • fetch-proxy --- Proxy requests to another server from within a fetch handler.
  • Deployment --- Deploy your Remix application to various platforms.

Released under the MIT License.