Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/remix-run/remix/llms.txt

Use this file to discover all available pages before exploring further.

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
}