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.