remix/node-fetch-server/test
Build Node.js servers with web-standard Fetch API primitives. node-fetch-server converts Node's HTTP server interfaces into Request/Response flows that match modern runtimes.
Features
- Web Standards - Standard
RequestandResponseAPIs - Node.js HTTP Integration - Works directly with
node:http,node:https, andnode: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, andX-Forwarded-Forheaders - Client Info - Access to client connection info (IP address, port, and trusted proxy address)
- TypeScript - Full TypeScript support with type definitions
Installation
npm i remixUsage
Use createRequestListener() when you want to plug a fetch handler into a standard Node.js server:
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: '[email protected]' },
{ id: '2', name: 'Bob', email: '[email protected]' },
])
}
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:
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:
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
Configure custom hostnames for deployment on VPS or custom environments. node-fetch-server uses the host option when constructing request.url.
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: protoandX-Forwarded-Protocan provide the original request protocol.Forwarded: hostandX-Forwarded-Hostcan provide the original request host.Forwarded: forandX-Forwarded-Forcan 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.
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:
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:
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:
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 RequestsendResponse(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
// 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 contains working demos:
demos/http2- HTTP/2 server with TLS certificates
Related Packages
fetch-proxy- Build HTTP proxy servers using the web fetch API
Benchmarks
Run the full benchmark suite:
pnpm run benchUpdate the benchmark results below:
pnpm run bench:update-readme
Last updated: 2026-04-29T17:19:30.407Z
Environment: Darwin 25.3.0, Apple M1 Pro, Node.js v24.15.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.15.0 |
47,110 |
10.66ms |
9.66MB |
remix/node-fetch-server |
0.13.0 |
43,317 |
11.69ms |
8.80MB |
express |
5.2.1 |
39,752 |
13.69ms |
9.59MB |
Small Body
POST benchmarks that read and print the request method, headers, and a small body.
| Server | Version | Requests/sec | Avg latency | Transfer/sec |
|---|---|---|---|---|
remix/node-fetch-server |
0.13.0 |
25,430 |
24.25ms |
5.17MB |
node:http |
24.15.0 |
25,088 |
23.89ms |
5.14MB |
express |
5.2.1 |
22,845 |
27.16ms |
5.51MB |
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 |
|---|---|---|---|---|
remix/node-fetch-server |
0.13.0 |
1,086 |
217.69ms |
225.87KB |
node:http |
24.15.0 |
1,079 |
198.67ms |
226.54KB |
express |
5.2.1 |
1,022 |
216.07ms |
252.51KB |
License
See LICENSE