Skip to main content
Response helper utilities for the web Fetch API. The response package provides focused helpers for common HTTP responses with correct headers and caching semantics.

Installation

npm i remix

Features

  • Web Standards Compliant - Built on the standard Response API
  • Runtime Agnostic - Works in Node.js, Bun, Deno, Cloudflare Workers, and browsers
  • File Responses - Full HTTP semantics including ETags, Last-Modified, conditional requests, and Range support
  • HTML Responses - Automatic DOCTYPE prepending and proper Content-Type headers
  • Redirect Responses - Simple redirect creation with customizable status codes
  • Compress Responses - Streaming compression based on Accept-Encoding header

Imports

This package provides no default export. Import specific helpers:
import { createFileResponse } from 'remix/response/file'
import { createHtmlResponse } from 'remix/response/html'
import { createRedirectResponse } from 'remix/response/redirect'
import { compressResponse } from 'remix/response/compress'

File Responses

Create responses for serving files with full HTTP semantics.

createFileResponse

function createFileResponse<file extends FileLike>(
  file: file,
  request: Request,
  options?: FileResponseOptions<file>
): Promise<Response>
file
FileLike
required
The file to serve (native File or LazyFile from @remix-run/lazy-file).
request
Request
required
The incoming request object (used for conditional requests and range support).
options
FileResponseOptions
Configuration options for the response.

FileResponseOptions

cacheControl
string
Cache-Control header value.Default: undefined (no Cache-Control header)
cacheControl: 'public, max-age=3600'
cacheControl: 'public, max-age=31536000, immutable' // hashed assets
etag
false | 'weak' | 'strong'
ETag generation strategy:
  • 'weak': Generates weak ETags based on file size and mtime (default)
  • 'strong': Generates strong ETags by hashing file content
  • false: Disables ETag generation
Default: 'weak'
digest
AlgorithmIdentifier | FileDigestFunction
Hash algorithm or custom digest function for strong ETags.
  • String: Web Crypto API algorithm name ('SHA-256', 'SHA-384', 'SHA-512', 'SHA-1')
  • Function: Custom digest computation
Only used when etag: 'strong'. Default: 'SHA-256'
digest: 'SHA-512'

// Custom digest function
digest: async (file) => {
  let hash = createHash('sha256')
  for await (let chunk of file.stream()) {
    hash.update(chunk)
  }
  return hash.digest('hex')
}
lastModified
boolean
Whether to include Last-Modified headers.Default: true
acceptRanges
boolean
Whether to support HTTP Range requests for partial content.When enabled, includes Accept-Ranges header and handles Range requests with 206 Partial Content responses.Defaults to enabling ranges only for non-compressible MIME types (as defined by isCompressibleMimeType() from @remix-run/mime).
Range requests and compression are mutually exclusive. This is why ranges are only enabled by default for non-compressible types.

Features

The createFileResponse helper automatically handles:
  • Content-Type and Content-Length headers
  • ETag generation (weak or strong)
  • Last-Modified headers
  • Cache-Control headers
  • Conditional requests (If-None-Match, If-Modified-Since, If-Match, If-Unmodified-Since)
  • Range requests for partial content (206 Partial Content)
  • HEAD request support

Examples

Basic File Response

import { createFileResponse } from 'remix/response/file'
import { openLazyFile } from 'remix/fs'

let lazyFile = openLazyFile('./public/image.jpg')
let response = await createFileResponse(lazyFile, request, {
  cacheControl: 'public, max-age=3600',
})

Strong ETags

For assets requiring strong validation:
let response = await createFileResponse(file, request, {
  etag: 'strong',
  digest: 'SHA-256',
})

Custom Digest Function

For large files or custom hashing:
import { createHash } from 'node:crypto'

let response = await createFileResponse(file, request, {
  etag: 'strong',
  async digest(file) {
    let hash = createHash('sha256')
    for await (let chunk of file.stream()) {
      hash.update(chunk)
    }
    return hash.digest('hex')
  },
})

Immutable Assets

For hashed assets that never change:
let response = await createFileResponse(file, request, {
  cacheControl: 'public, max-age=31536000, immutable',
  etag: 'strong',
})

FileLike Interface

The minimal interface for file-like objects:
interface FileLike {
  readonly name: string
  readonly size: number
  readonly type: string
  readonly lastModified: number
  stream(): ReadableStream<Uint8Array>
  arrayBuffer(): Promise<ArrayBuffer>
  slice(start?: number, end?: number, contentType?: string): {
    stream(): ReadableStream<Uint8Array>
  }
}
Both native File objects and LazyFile from @remix-run/lazy-file implement this interface.

HTML Responses

Create HTML responses with proper Content-Type and DOCTYPE handling.

createHtmlResponse

function createHtmlResponse(
  html: string | SafeHtml | Blob | File | ArrayBuffer | ReadableStream,
  init?: ResponseInit
): Response
html
string | SafeHtml | Blob | File | ArrayBuffer | ReadableStream
required
The HTML content to send. Accepts multiple types:
  • String
  • SafeHtml from @remix-run/html-template
  • Blob/File
  • ArrayBuffer
  • ReadableStream
init
ResponseInit
Standard Response initialization options (status, headers, etc.).
returns
Response
A Response with Content-Type: text/html; charset=UTF-8 and automatic DOCTYPE prepending if not present.

Examples

Basic HTML Response

import { createHtmlResponse } from 'remix/response/html'

let response = createHtmlResponse('<h1>Hello, World!</h1>')
// Content-Type: text/html; charset=UTF-8
// Body: <!DOCTYPE html><h1>Hello, World!</h1>

With SafeHtml

import { html } from 'remix/html-template'
import { createHtmlResponse } from 'remix/response/html'

let name = '<script>alert(1)</script>'
let response = createHtmlResponse(
  html`<h1>Hello, ${name}!</h1>`
)
// Safely escaped HTML

With Custom Status

let response = createHtmlResponse(
  html`<h1>Not Found</h1>`,
  { status: 404 }
)

Complete Page

import { html } from 'remix/html-template'

function renderPage(title: string, content: string) {
  return createHtmlResponse(
    html`
      <!doctype html>
      <html lang="en">
        <head>
          <meta charset="UTF-8" />
          <title>${title}</title>
        </head>
        <body>
          <main>${content}</main>
        </body>
      </html>
    `
  )
}

Redirect Responses

Create redirect responses with improved ergonomics over Response.redirect.

createRedirectResponse

function createRedirectResponse(
  location: string,
  init?: number | ResponseInit
): Response
location
string
required
The redirect destination. Can be relative or absolute.
init
number | ResponseInit
Status code (number) or ResponseInit object with additional options.Default: 302
Unlike Response.redirect, this helper accepts relative URLs. While not technically spec-compliant, relative redirects are widely supported and commonly used.

Examples

Basic Redirect

import { createRedirectResponse } from 'remix/response/redirect'

// Default 302 redirect
let response = createRedirectResponse('/login')

Custom Status Code

// 301 Permanent Redirect
let response = createRedirectResponse('/new-page', 301)

// 303 See Other
let response = createRedirectResponse('/dashboard', 303)

With Additional Headers

let response = createRedirectResponse('/dashboard', {
  status: 303,
  headers: {
    'X-Redirect-Reason': 'authentication',
    'Set-Cookie': await cookie.serialize(value),
  },
})

Common Redirect Patterns

// After form submission (POST -> GET)
let response = createRedirectResponse('/success', 303)

// Permanent URL change
let response = createRedirectResponse('/new-url', 301)

// Temporary redirect (default)
let response = createRedirectResponse('/maintenance', 302)

// After login
let response = createRedirectResponse('/dashboard', {
  status: 303,
  headers: {
    'Set-Cookie': await sessionCookie.serialize(sessionData),
  },
})

Compress Responses

Compress responses based on the client’s Accept-Encoding header.

compressResponse

function compressResponse(
  response: Response,
  request: Request,
  options?: CompressResponseOptions
): Promise<Response>
response
Response
required
The response to compress.
request
Request
required
The incoming request (used to read Accept-Encoding header).
options
CompressResponseOptions
Compression configuration options.

CompressResponseOptions

threshold
number
Minimum size in bytes to compress (only enforced if Content-Length is present).Default: 1024
encodings
Encoding[]
Which encodings the server supports for negotiation.Default: ['br', 'gzip', 'deflate']
zlib
object
Node.js zlib options for gzip/deflate compression.For SSE responses (text/event-stream), flush: Z_SYNC_FLUSH is automatically applied unless you explicitly set a flush value.Node.js zlib options reference
brotli
object
Node.js zlib options for Brotli compression.For SSE responses (text/event-stream), flush: BROTLI_OPERATION_FLUSH is automatically applied unless you explicitly set a flush value.Node.js Brotli options reference

Automatic Skip Conditions

Compression is automatically skipped for:
  • Responses with no Accept-Encoding header
  • Responses already compressed (existing Content-Encoding)
  • Responses with Cache-Control: no-transform
  • Responses with Content-Length below threshold
  • Responses with range support (Accept-Ranges: bytes)
  • 206 Partial Content responses
  • HEAD requests (only headers are modified)

Examples

Basic Compression

import { compressResponse } from 'remix/response/compress'

let response = new Response(JSON.stringify(data), {
  headers: { 'Content-Type': 'application/json' },
})

let compressed = await compressResponse(response, request)

Custom Threshold

let compressed = await compressResponse(response, request, {
  threshold: 2048, // Only compress if > 2KB
})

Custom Encodings

// Only support gzip and deflate
let compressed = await compressResponse(response, request, {
  encodings: ['gzip', 'deflate'],
})

Custom Compression Options

import * as zlib from 'node:zlib'

let compressed = await compressResponse(response, request, {
  zlib: {
    level: 9, // Maximum compression
  },
  brotli: {
    params: {
      [zlib.constants.BROTLI_PARAM_QUALITY]: 11, // Maximum quality
    },
  },
})

SSE (Server-Sent Events)

// Compression is automatically configured for streaming
let sseResponse = new Response(stream, {
  headers: { 'Content-Type': 'text/event-stream' },
})

let compressed = await compressResponse(sseResponse, request)
// Automatically uses Z_SYNC_FLUSH for gzip/deflate
// and BROTLI_OPERATION_FLUSH for Brotli

Range Requests and Compression

Range requests and compression are mutually exclusive. When Accept-Ranges: bytes is present, compressResponse will not compress the response.
This is why createFileResponse enables ranges only for non-compressible MIME types by default:
// Text file: ranges disabled, compression possible
let textFile = new File(['content'], 'file.txt', { type: 'text/plain' })
let response = await createFileResponse(textFile, request)
// Can be compressed

// Video file: ranges enabled, compression skipped
let videoFile = new File([data], 'video.mp4', { type: 'video/mp4' })
let response = await createFileResponse(videoFile, request)
// Supports range requests, not compressed

Type Definitions

interface FileLike {
  readonly name: string
  readonly size: number
  readonly type: string
  readonly lastModified: number
  stream(): ReadableStream<Uint8Array>
  arrayBuffer(): Promise<ArrayBuffer>
  slice(start?: number, end?: number, contentType?: string): {
    stream(): ReadableStream<Uint8Array>
  }
}

type FileDigestFunction<file extends FileLike = File> = (
  file: file
) => Promise<string>

interface FileResponseOptions<file extends FileLike = File> {
  cacheControl?: string
  etag?: false | 'weak' | 'strong'
  digest?: AlgorithmIdentifier | FileDigestFunction<file>
  lastModified?: boolean
  acceptRanges?: boolean
}

type Encoding = 'br' | 'gzip' | 'deflate'

interface CompressResponseOptions {
  threshold?: number
  encodings?: Encoding[]
  zlib?: object
  brotli?: object
}

Build docs developers (and LLMs) love