# @remix-run/node-fetch-server

> Build servers for Node.js using the web fetch API

Latest version **1.0.0** (published 2026-10-01) · MIT license · 0 weekly downloads

## Install

```sh
npm install @remix-run/node-fetch-server
pnpm add @remix-run/node-fetch-server
yarn add @remix-run/node-fetch-server
bun add @remix-run/node-fetch-server
```

## Health

**Score 70/100 (B)** — status: active.

Positive: esm support; no vulnerabilities; has provenance; recently updated; high maintenance score; popular repo.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 1.0.0 |
| Published | 2026-10-01 |
| First published | 2025-07-24 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | ESM |
| Dependencies | 0 |
| Unpacked size | 81 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 33414 |
| Author | Michael Jackson |
| Maintainers | mjackson |
| Keywords | http, server, request, response, fetch, web |

## Links

- npm: https://www.npmjs.com/package/@remix-run/node-fetch-server
- Repository: https://github.com/remix-run/remix
- Homepage: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server#readme
- Issues: https://github.com/remix-run/remix/issues
- npm.io page: https://npm.io/package/@remix-run/node-fetch-server

## Alternatives

- [launchdarkly-js-client-sdk](https://npm.io/package/launchdarkly-js-client-sdk.md) — 2.5M weekly downloads
- [@elastic/elasticsearch](https://npm.io/package/@elastic/elasticsearch.md) — 2.1M weekly downloads
- [@c8y/client](https://npm.io/package/@c8y/client.md) — 15.3K weekly downloads
- [@signaldb/maverickjs](https://npm.io/package/@signaldb/maverickjs.md) — 1.7K weekly downloads
- [@bbc/http-transport-cache](https://npm.io/package/@bbc/http-transport-cache.md) — 1.2K weekly downloads

## Recent versions

- 1.0.0 (latest) — 2026-10-01
- 0.14.2 — 2026-09-28
- 0.14.1 — 2026-08-14
- 0.14.0 — 2026-07-01
- 0.13.3 — 2026-05-20
- 0.13.2 — 2026-05-20
- 0.13.1 — 2026-04-30
- 0.13.0 — 2025-12-18
- 0.12.0 — 2025-11-04
- 0.11.0 — 2025-10-22
- 0.10.0 — 2025-10-04
- 0.9.0 — 2025-09-16
- 0.8.1 — 2025-09-11
- 0.8.0 — 2025-07-24

## README

# node-fetch-server

Build Node.js servers with web-standard Fetch API primitives. `node-fetch-server` converts Node's HTTP server interfaces into [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)/[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) flows that match modern runtimes.

## Features

- **Web Standards** - Standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs
- **Node.js HTTP Integration** - Works directly with `node:http`, `node:https`, and `node:http2`
- **Streaming Support** - Response support with `ReadableStream`
- **Custom Hostname** - Configuration for deployment flexibility
- **Proxy Header Support** - Opt in to trusted `Forwarded`, `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-For` headers
- **Client Info** - Access to client connection info (IP address, port, and trusted proxy address)
- **TypeScript** - Full TypeScript support with type definitions

## Installation

```sh
npm i remix
```

## Usage

Use `createRequestListener()` when you want to plug a fetch handler into a standard Node.js server:

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

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([
      { id: '1', name: 'Alice', email: 'alice@example.com' },
      { id: '2', name: 'Bob', email: 'bob@example.com' },
    ])
  }

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

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

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000')
})
```

### Working with Request Data

Handle different types of request data using standard web APIs:

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

  // Handle JSON data
  if (request.method === 'POST' && url.pathname === '/api/users') {
    try {
      let userData = await request.json()

      // Validate required fields
      if (!userData.name || !userData.email) {
        return Response.json({ error: 'Name and email are required' }, { status: 400 })
      }

      // Create user (your implementation)
      let newUser = {
        id: Date.now().toString(),
        ...userData,
      }

      return Response.json(newUser, { status: 201 })
    } catch (error) {
      return Response.json({ error: 'Invalid JSON' }, { status: 400 })
    }
  }

  // Handle URL search params
  if (url.pathname === '/api/search') {
    let query = url.searchParams.get('q')
    let limit = parseInt(url.searchParams.get('limit') || '10')

    return Response.json({
      query,
      limit,
      results: [], // Your search results here
    })
  }

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

### Streaming Responses

Take advantage of web-standard streaming with `ReadableStream`:

```ts
async function handler(request: Request) {
  if (request.url.endsWith('/stream')) {
    // Create a streaming response
    let stream = new ReadableStream({
      async start(controller) {
        for (let i = 0; i < 5; i++) {
          controller.enqueue(new TextEncoder().encode(`Chunk ${i}\n`))
          await new Promise((resolve) => setTimeout(resolve, 1000))
        }
        controller.close()
      },
    })

    return new Response(stream, {
      headers: { 'Content-Type': 'text/plain' },
    })
  }

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

### Custom Hostname Configuration

Request URLs use HTTP/2 `:authority` when present, falling back to `Host`. If both identify different hosts, `createRequestListener()` rejects the request before calling the handler, and `createRequest()` throws. The listener calls `onError` and defaults to 400 when no custom response is returned. Hostnames and default ports are normalized before comparison.

Configure custom hostnames for deployment on VPS or custom environments. `node-fetch-server` uses the `host` option when constructing `request.url`.

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

let hostname = process.env.HOST || 'api.example.com'

async function handler(request: Request) {
  console.log(request.url) // http://api.example.com/path

  return Response.json({
    message: 'Hello from custom domain!',
    url: request.url,
  })
}

let server = http.createServer(createRequestListener(handler, { host: hostname }))

server.listen(3000)
```

### Trusted Proxy Headers

If your app runs behind a trusted reverse proxy, Node.js sees the proxy connection instead of the original client connection. Enable `trustProxy` to use trusted proxy headers when constructing `request.url` and client information:

- `Forwarded: proto` and `X-Forwarded-Proto` can provide the original request protocol.
- `Forwarded: host` and `X-Forwarded-Host` can provide the original request host.
- `Forwarded: for` and `X-Forwarded-For` can provide the original client address.

Only enable this option when your server is reachable exclusively through a trusted proxy that overwrites these headers. Otherwise, clients can spoof the host, protocol, and client address.

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

let server = http.createServer(
  createRequestListener(handler, {
    trustProxy: true,
  }),
)

server.listen(3000)
```

When `host` or `protocol` are set, those fixed options take precedence over trusted proxy headers.

### Accessing Client Information

Get client connection details (IP address, port) for logging or security. When `trustProxy` is enabled, `client.address` uses trusted `Forwarded` or `X-Forwarded-For` values when present:

```ts
import { type FetchHandler } from 'remix/node-fetch-server'

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

  // Use for rate limiting, geolocation, etc.
  if (isRateLimited(client.address)) {
    return new Response('Too Many Requests', { status: 429 })
  }

  return Response.json({
    message: 'Hello!',
    yourIp: client.address,
  })
}
```

### HTTPS Support

Use with Node.js HTTPS module for secure connections:

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

let options = {
  key: fs.readFileSync('private-key.pem'),
  cert: fs.readFileSync('certificate.pem'),
}

let server = https.createServer(options, createRequestListener(handler))

server.listen(443, () => {
  console.log('HTTPS Server running on port 443')
})
```

## Advanced Usage

### Low-level API

For more control over request/response handling, use the low-level API:

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

let server = http.createServer(async (req, res) => {
  // Convert Node.js request to Fetch API Request
  let request = createRequest(req, res, { host: process.env.HOST })

  try {
    // Add custom headers or middleware logic
    let startTime = Date.now()

    // Process the request with your handler
    let response = await handler(request)
    // Make sure the response is mutable
    response = new Response(response.body, response)

    // Add response timing header
    let duration = Date.now() - startTime
    response.headers.set('X-Response-Time', `${duration}ms`)

    // Send the response
    await sendResponse(res, response)
  } catch (error) {
    console.error('Server error:', error)
    res.writeHead(500, { 'Content-Type': 'text/plain' })
    res.end('Internal Server Error')
  }
})

server.listen(3000)
```

The low-level API provides:

- `createRequest(req, res, options)` - Converts Node.js IncomingMessage to web Request
- `sendResponse(res, response)` - Sends web Response using Node.js ServerResponse

This is useful for:

- Building custom middleware systems
- Integrating with existing Node.js code
- Implementing custom error handling
- Performance-critical applications

## Migration from Express

Transitioning from Express? Here's a comparison of common patterns:

### Basic Routing

```ts
// Express
let app = express()

app.get('/users/:id', async (req, res) => {
  let user = await db.getUser(req.params.id)
  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }
  res.json(user)
})

app.listen(3000)

// node-fetch-server
import { createRequestListener } from 'remix/node-fetch-server'

async function handler(request: Request) {
  let url = new URL(request.url)
  let match = url.pathname.match(/^\/users\/(\w+)$/)

  if (match && request.method === 'GET') {
    let user = await db.getUser(match[1])
    if (!user) {
      return Response.json({ error: 'User not found' }, { status: 404 })
    }
    return Response.json(user)
  }

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

http.createServer(createRequestListener(handler)).listen(3000)
```

## Demos

The [`demos` directory](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos) contains working demos:

- [`demos/http2`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos/http2) - HTTP/2 server with TLS certificates

## Related Packages

- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) - Build HTTP proxy servers using the web fetch API

## Benchmarks

Run the full benchmark suite:

```sh
pnpm run bench
```

Update the benchmark results below:

```sh
pnpm run bench:update-readme
```

<!-- benchmarks:start -->

Last updated: 2026-07-23T20:29:32.942Z

Environment: Darwin 25.4.0, Apple M5 Pro, Node.js v24.18.0

Command: `wrk -t12 -c400 -d30s`

### Raw Throughput

Simple HTML response benchmarks without inspecting the incoming request.

| Server                    |   Version | Requests/sec | Avg latency | Transfer/sec |
| ------------------------- | --------: | -----------: | ----------: | -----------: |
| `node:http`               | `24.18.0` |     `66,594` |    `5.96ms` |    `13.65MB` |
| `remix/node-fetch-server` |  `0.14.0` |     `61,587` |    `7.88ms` |    `12.51MB` |
| `express`                 |   `5.2.1` |     `58,424` |    `8.42ms` |    `14.10MB` |

### Small Body

POST benchmarks that read and print the request method, headers, and a small body.

| Server                    |   Version | Requests/sec | Avg latency | Transfer/sec |
| ------------------------- | --------: | -----------: | ----------: | -----------: |
| `node:http`               | `24.18.0` |     `35,303` |   `15.60ms` |     `7.24MB` |
| `express`                 |   `5.2.1` |     `32,614` |   `16.93ms` |     `7.87MB` |
| `remix/node-fetch-server` |  `0.14.0` |     `29,521` |   `18.98ms` |     `6.00MB` |

### Large Body

POST benchmarks that read and print the request method, headers, and a 1 MB body.

| Server                    |   Version | Requests/sec | Avg latency | Transfer/sec |
| ------------------------- | --------: | -----------: | ----------: | -----------: |
| `node:http`               | `24.18.0` |      `1,798` |  `206.65ms` |   `377.42KB` |
| `remix/node-fetch-server` |  `0.14.0` |      `1,752` |  `167.69ms` |   `364.40KB` |
| `express`                 |   `5.2.1` |      `1,731` |  `223.19ms` |   `427.67KB` |

<!-- benchmarks:end -->

## License

See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)

---
_Source: https://npm.io/package/@remix-run/node-fetch-server · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
