Skip to content

fetch-proxy

The fetch-proxy package creates a fetch-compatible function that forwards requests to another server. It handles URL rewriting, request forwarding, and automatic Set-Cookie domain/path rewriting so proxied cookies work correctly on the client.

Common use cases include proxying API requests to a backend service, forwarding to a development server, and building reverse proxy middleware.

Installation

bash
# Via the meta-package
npm install remix

# Or individually
npm install @remix-run/fetch-proxy

Imports

ts
import { createFetchProxy } from 'remix/fetch-proxy'

// Types
import type { FetchProxy, FetchProxyOptions } from 'remix/fetch-proxy'

createFetchProxy(target, options?)

Creates a fetch function that forwards requests to the specified target server.

ts
import { createFetchProxy } from 'remix/fetch-proxy'

let proxy = createFetchProxy('http://api.internal:8080')

Parameters

ParameterTypeDescription
targetstring | URLThe base URL of the server to forward requests to.
optionsFetchProxyOptionsOptional configuration for proxy behavior.

Returns

A FetchProxy function with the same signature as the global fetch.


FetchProxy

The proxy function returned by createFetchProxy(). It has the same signature as the standard fetch function.

ts
interface FetchProxy {
  (input: URL | RequestInfo, init?: RequestInit): Promise<Response>
}

FetchProxyOptions

OptionTypeDefaultDescription
fetchtypeof globalThis.fetchglobalThis.fetchCustom fetch implementation to use for the actual request.
rewriteCookieDomainbooleantrueRewrites the Domain attribute of Set-Cookie headers to match the incoming request's domain.
rewriteCookiePathbooleantrueRemoves the proxy target's pathname prefix from the Path attribute of Set-Cookie headers.
xForwardedHeadersbooleanfalseAdds X-Forwarded-Proto and X-Forwarded-Host headers to the proxied request.

URL Rewriting

The proxy rewrites URLs by prepending the target's origin and pathname to the incoming request's pathname.

ts
let proxy = createFetchProxy('http://api.internal:8080/v1')

// Request to /users becomes http://api.internal:8080/v1/users
let response = await proxy('http://localhost:3000/users')

Query strings are preserved:

ts
// Becomes http://api.internal:8080/v1/search?q=remix
await proxy('http://localhost:3000/search?q=remix')

By default, Set-Cookie headers in the proxied response are rewritten so they work correctly on the client.

Domain Rewriting

When rewriteCookieDomain is true (the default), the Domain attribute of Set-Cookie headers is changed to match the incoming request's host:

# Original from target server:
Set-Cookie: session=abc; Domain=api.internal; Path=/

# Rewritten to:
Set-Cookie: session=abc; Domain=localhost:3000; Path=/

Path Rewriting

When rewriteCookiePath is true (the default), the target's pathname prefix is stripped from the Path attribute:

# Target: http://api.internal:8080/v1
# Original from target:
Set-Cookie: token=xyz; Path=/v1/auth

# Rewritten to:
Set-Cookie: token=xyz; Path=/auth

If the cookie path exactly matches the target pathname, it is rewritten to /.

ts
let proxy = createFetchProxy('http://api.internal:8080', {
  rewriteCookieDomain: false,
  rewriteCookiePath: false,
})

Forwarded Headers

Enable xForwardedHeaders to add standard proxy headers to the forwarded request:

ts
let proxy = createFetchProxy('http://api.internal:8080', {
  xForwardedHeaders: true,
})

This adds:

  • X-Forwarded-Proto: The protocol of the original request (e.g. https).
  • X-Forwarded-Host: The host of the original request (e.g. example.com).

These headers let the target server know the original client protocol and host, which is important for generating correct URLs in responses.


Request Forwarding

All properties of the original request are forwarded to the target:

  • HTTP method
  • Headers
  • Body (streamed, not buffered)
  • Cache, credentials, integrity, keepalive, mode, redirect, referrer, and referrerPolicy settings
  • Abort signal

The request body is forwarded as a stream using the duplex: 'half' option, so large request bodies are not buffered in memory.


Usage with the Router

As a Route Handler

Proxy specific routes to a backend service:

ts
import { createRouter } from 'remix/fetch-router'
import { createFetchProxy } from 'remix/fetch-proxy'

let apiProxy = createFetchProxy('http://api.internal:8080')

let router = createRouter()

router.get('/api/*path', ({ request }) => {
  return apiProxy(request)
})

As Development Proxy

Forward requests to a separate development server:

ts
import { createFetchProxy } from 'remix/fetch-proxy'

let devProxy = createFetchProxy('http://localhost:5173', {
  xForwardedHeaders: true,
})

let router = createRouter()

// Proxy static assets to Vite dev server
router.get('/assets/*path', ({ request }) => {
  return devProxy(request)
})

With Custom Fetch

Use a custom fetch implementation (e.g. for testing or to add default headers):

ts
let proxy = createFetchProxy('http://api.internal:8080', {
  fetch(input, init) {
    let request = new Request(input, init)
    request.headers.set('Authorization', `Bearer ${process.env.API_TOKEN}`)
    return globalThis.fetch(request)
  },
})

Complete Example

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

// Create proxies for backend services
let authProxy = createFetchProxy('http://auth-service:4000', {
  xForwardedHeaders: true,
})

let apiProxy = createFetchProxy('http://api-service:5000/v2', {
  xForwardedHeaders: true,
})

// Define routes
let routes = route({
  home: '/',
  auth: '/auth/*path',
  api: '/api/*path',
})

// Create router
let router = createRouter()

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

// Proxy auth routes
router.map(routes.auth, ({ request }) => {
  return authProxy(request)
})

// Proxy API routes
router.map(routes.api, ({ request }) => {
  return apiProxy(request)
})

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

  • fetch-router --- The router for defining routes that proxy to backend services.
  • node-fetch-server --- Run the proxy gateway on Node.js.
  • headers --- The SetCookie class used internally for cookie rewriting.

Released under the MIT License.