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

Features

  • Web Standards Compliant: Built on the standard Response API, works in any JavaScript runtime
  • 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

Installation

npm i remix

Usage

This package provides no default export. Instead, import the specific helper you need:
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

The createFileResponse helper creates a response for serving files with full HTTP semantics. It works with both native File objects and LazyFile from @remix-run/lazy-file:
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',
})

Features

  • 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

Options

await createFileResponse(file, request, {
  // Cache-Control header value.
  // Defaults to `undefined` (no Cache-Control header).
  cacheControl: 'public, max-age=3600',

  // 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
  etag: 'weak',

  // Hash algorithm for strong ETags (Web Crypto API algorithm names).
  // Only used when etag: 'strong'.
  // Defaults to 'SHA-256'.
  digest: 'SHA-256',

  // Whether to generate Last-Modified headers.
  // Defaults to `true`.
  lastModified: true,

  // Whether to support HTTP Range requests for partial content.
  // Defaults to `true`.
  acceptRanges: true,
})

Strong ETags and Content Hashing

For assets that require strong validation (e.g., to support If-Match preconditions), configure strong ETag generation:
return createFileResponse(file, request, {
  etag: 'strong',
})
By default, strong ETags are generated using the Web Crypto API with the 'SHA-256' algorithm. You can customize this:
return createFileResponse(file, request, {
  etag: 'strong',
  // Specify a different hash algorithm
  digest: 'SHA-512',
})
For large files or custom hashing requirements, provide a custom digest function:
await createFileResponse(file, request, {
  etag: 'strong',
  async digest(file) {
    // Custom streaming hash for large files
    let { createHash } = await import('node:crypto')
    let hash = createHash('sha256')
    for await (let chunk of file.stream()) {
      hash.update(chunk)
    }
    return hash.digest('hex')
  },
})

Conditional Requests

createFileResponse automatically handles conditional requests:
import { createFileResponse } from 'remix/response/file'
import { openLazyFile } from 'remix/fs'

async function handler(request: Request) {
  let file = openLazyFile('./public/document.pdf')
  
  // Automatically handles If-None-Match, If-Modified-Since, etc.
  let response = await createFileResponse(file, request, {
    cacheControl: 'public, max-age=86400',
  })
  
  // Response may be 304 Not Modified if conditions match
  return response
}

Range Requests

Support partial content delivery for large files:
import { createFileResponse } from 'remix/response/file'
import { openLazyFile } from 'remix/fs'

async function handler(request: Request) {
  let file = openLazyFile('./public/video.mp4')
  
  // Automatically handles Range header
  let response = await createFileResponse(file, request, {
    acceptRanges: true,
  })
  
  // Response may be 206 Partial Content if Range header is present
  return response
}

HTML Responses

The createHtmlResponse helper creates HTML responses with proper Content-Type and DOCTYPE handling:
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>
The helper automatically prepends <!DOCTYPE html> if not already present. It works with strings, SafeHtml from @remix-run/html-template, Blobs/Files, ArrayBuffers, and ReadableStreams.

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

Complete Pages

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

async function handler(request: Request) {
  let user = await getUser(request)
  
  return createHtmlResponse(html`
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>Dashboard</title>
      </head>
      <body>
        <h1>Welcome, ${user.name}!</h1>
      </body>
    </html>
  `)
}

Redirect Responses

The createRedirectResponse helper creates redirect responses. The main improvements over the native Response.redirect API are:
  • Accepts a relative location instead of a full URL
  • Accepts a ResponseInit object as the second argument, allowing you to set additional headers and status code
import { createRedirectResponse } from 'remix/response/redirect'

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

// Custom status code
let response = createRedirectResponse('/new-page', 301)

// With additional headers
let response = createRedirectResponse('/dashboard', {
  status: 303,
  headers: { 'X-Redirect-Reason': 'authentication' },
})

Common Redirect Patterns

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

// After form submission (303 See Other)
router.post('/submit', async (context) => {
  await processForm(context)
  return createRedirectResponse('/success', 303)
})

// Permanent redirect (301 Moved Permanently)
router.get('/old-page', () => {
  return createRedirectResponse('/new-page', 301)
})

// Temporary redirect (302 Found - default)
router.get('/temp', () => {
  return createRedirectResponse('/temporary-location')
})

Compress Responses

The compressResponse helper compresses a Response based on the client’s Accept-Encoding header:
import { compressResponse } from 'remix/response/compress'

let response = new Response(JSON.stringify(data), {
  headers: { 'Content-Type': 'application/json' },
})
let compressed = await compressResponse(response, request)
Compression is automatically skipped for:
  • Responses with no Accept-Encoding header
  • Responses that are already compressed (existing Content-Encoding)
  • Responses with Cache-Control: no-transform
  • Responses with Content-Length below threshold (default: 1024 bytes)
  • Responses with range support (Accept-Ranges: bytes)
  • 206 Partial Content responses
  • HEAD requests (only headers are modified)

Options

await compressResponse(response, request, {
  // Minimum size in bytes to compress (only enforced if Content-Length is present).
  // Default: 1024
  threshold: 1024,

  // Which encodings the server supports for negotiation.
  // Defaults to ['br', 'gzip', 'deflate']
  encodings: ['br', 'gzip', 'deflate'],

  // node: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.
  zlib: {
    level: 6,
  },

  // node: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.
  brotli: {
    params: {
      [zlib.constants.BROTLI_PARAM_QUALITY]: 4,
    },
  },
})

Usage with Middleware

import { compressResponse } from 'remix/response/compress'
import { createRouter } from 'remix/fetch-router'

let router = createRouter()

// Add compression middleware
router.use(async (context, next) => {
  let response = await next()
  return compressResponse(response, context.request)
})

router.get('/api/data', () => {
  return Response.json({ large: 'data object' })
})

API Reference

createFileResponse(file, request, options?)

Create a response for serving files with full HTTP semantics. Parameters:
  • file: File - The file to serve
  • request: Request - The incoming request
  • options?: FileResponseOptions - Configuration options
Returns: Promise<Response>

createHtmlResponse(html, init?)

Create an HTML response with proper Content-Type. Parameters:
  • html: string | SafeHtml | Blob | ArrayBuffer | ReadableStream - The HTML content
  • init?: ResponseInit - Additional response options
Returns: Response

createRedirectResponse(location, init?)

Create a redirect response. Parameters:
  • location: string - The redirect location (can be relative)
  • init?: number | ResponseInit - Status code or response options (default: 302)
Returns: Response

compressResponse(response, request, options?)

Compress a response based on Accept-Encoding. Parameters:
  • response: Response - The response to compress
  • request: Request - The incoming request
  • options?: CompressOptions - Compression configuration
Returns: Promise<Response>

Build docs developers (and LLMs) love