Request & Response
Every Remix handler revolves around two objects: a Request (what the user sent) and a Response (what you send back). Both are standard Web APIs --- the same classes used in browsers, Service Workers, and Cloudflare Workers.
This guide covers everything you need to know about working with these objects in Remix.
The Fetch API Request Object
The Request object represents an incoming HTTP request. It is defined by the Fetch API specification and is available in all modern JavaScript runtimes.
Key Properties
| Property | Type | Description |
|---|---|---|
request.method | string | The HTTP method: "GET", "POST", "PUT", "DELETE", etc. |
request.url | string | The full URL, e.g. "https://example.com/books?page=2" |
request.headers | Headers | The request headers (a Map-like object) |
request.body | `ReadableStream | null` |
Reading Headers
handler({ request }) {
let contentType = request.headers.get('Content-Type')
let authorization = request.headers.get('Authorization')
let userAgent = request.headers.get('User-Agent')
// Check if a header exists
if (request.headers.has('X-Custom-Header')) {
// ...
}
}Reading the Request Body
The Request object provides several methods to read the body. Each can only be called once because the body is a stream that is consumed when read.
// Read as JSON
let data = await request.json()
// Read as plain text
let text = await request.text()
// Read as FormData
let formData = await request.formData()
// Read as ArrayBuffer (binary data)
let buffer = await request.arrayBuffer()
// Read as Blob
let blob = await request.blob()Body can only be read once
The request body is a stream. Once you read it with .json(), .text(), or any other method, it is consumed and cannot be read again. If multiple parts of your code need the body, read it once and pass the result around. The formData() middleware handles this for form submissions.
Checking the Method
handler({ request }) {
if (request.method === 'GET') {
// Return a page
} else if (request.method === 'POST') {
// Process a form submission
}
}In practice, you rarely check the method manually because Remix routes are already bound to specific methods (via get(), post(), form(), etc.).
The Fetch API Response Object
The Response object represents the HTTP response you send back to the client. You create one with new Response():
new Response(body, options)Creating Basic Responses
// Plain text
new Response('Hello, world!')
// With status code and headers
new Response('Not Found', {
status: 404,
headers: { 'Content-Type': 'text/plain' },
})
// Empty response (for redirects, 204 No Content, etc.)
new Response(null, { status: 204 })Response Properties
| Property | Type | Description |
|---|---|---|
response.status | number | The HTTP status code (200, 404, 500, etc.) |
response.statusText | string | The status text ("OK", "Not Found", etc.) |
response.headers | Headers | The response headers |
response.body | `ReadableStream | null` |
response.ok | boolean | true if status is 200-299 |
JSON Responses
// Return JSON data
return Response.json({ title: 'Ash & Smoke', price: 16.99 })
// With custom status
return Response.json(
{ error: 'Book not found' },
{ status: 404 },
)Response.json() automatically sets the Content-Type header to application/json.
HTML Responses
// Manual HTML response
return new Response('<h1>Hello</h1>', {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})For convenience, Remix provides the createHtmlResponse helper:
import { createHtmlResponse } from 'remix/response/html'
return createHtmlResponse('<h1>Hello</h1>')
// With custom status
return createHtmlResponse('<h1>Not Found</h1>', { status: 404 })Redirect Responses
Redirects tell the browser to navigate to a different URL:
// Manual redirect
return new Response(null, {
status: 302,
headers: { Location: '/login' },
})Remix provides a helper:
import { createRedirectResponse } from 'remix/response/redirect'
// 302 Found (temporary redirect, the default)
return createRedirectResponse('/login')
// 301 Moved Permanently
return createRedirectResponse('/new-url', 301)Common redirect status codes:
| Code | Name | When to Use |
|---|---|---|
| 301 | Moved Permanently | The URL has permanently changed (browsers cache this) |
| 302 | Found | Temporary redirect (the default) |
| 303 | See Other | Redirect after a POST (forces GET) |
| 307 | Temporary Redirect | Like 302 but preserves the HTTP method |
| 308 | Permanent Redirect | Like 301 but preserves the HTTP method |
File Responses
For serving files (downloads, images, etc.):
import { createFileResponse } from 'remix/response/file'
// Serve a file from disk
return createFileResponse('/path/to/file.pdf', request)createFileResponse handles:
- Setting the correct
Content-Typebased on the file extension - Setting
Content-Length - Supporting range requests (for partial downloads and media streaming)
- Supporting
If-None-MatchandIf-Modified-Sincefor caching
RequestContext in Remix
In Remix handlers, you do not work with the raw Request object alone. Instead, you receive a RequestContext that wraps the request with additional functionality:
handler({ request, url, params, get, set }) {
// The raw Fetch API Request
request.method // "GET"
// A parsed URL object (no need to parse request.url yourself)
url.pathname // "/books/great-gatsby"
url.searchParams.get('page') // "2"
// Route parameters (from dynamic segments)
params.slug // "great-gatsby"
// Context values set by middleware
let db = get(Database)
let session = get(Session)
}request
The original Fetch API Request object.
url
A pre-parsed URL object created from request.url. Saves you from writing new URL(request.url) in every handler.
Useful properties:
url.pathname--- The path portion (e.g./books/fiction)url.searchParams--- AURLSearchParamsobject for query string accessurl.origin--- The scheme + host (e.g.https://example.com)url.host--- The hostname and port (e.g.example.com:3000)
params
An object containing the values of dynamic segments and wildcards from the matched route:
// Route: '/users/:userId/posts/:postId'
// URL: '/users/5/posts/42'
params.userId // "5"
params.postId // "42"get(key) and set(key, value)
Read and write values in the request context. This is how middleware and handlers share data:
// In middleware
context.set(Database, db)
// In handler
let db = get(Database)See the Middleware guide for details on context keys.
Reading Form Data
For form submissions, the formData() middleware parses the request body and makes it available via the context:
import { formData } from 'remix/form-data-middleware'
// In middleware setup
let router = createRouter({
middleware: [formData()],
})
// In handler
handler({ get }) {
let data = get(FormData)
let name = data.get('name') // string | File | null
let email = data.get('email') // string | File | null
let tags = data.getAll('tags') // (string | File)[]
}FormData is the standard Web API FormData interface:
| Method | Description |
|---|---|
data.get('key') | Get a single value by name |
data.getAll('key') | Get all values for a name (for multi-select, checkboxes) |
data.has('key') | Check if a key exists |
data.entries() | Iterate over all key-value pairs |
To convert FormData to a plain object:
let values = Object.fromEntries(data) as Record<string, string>Reading JSON Bodies
For API endpoints that receive JSON:
handler({ request }) {
let body = await request.json()
// body is typed as `any` — validate it before use
}Always validate incoming data
Whether it comes from a form or a JSON body, always validate incoming data on the server. Use data-schema for structured validation:
import { object, string, parseSafe } from 'remix/data-schema'
let schema = object({ name: string(), email: string() })
let result = parseSafe(schema, body)
if (!result.success) {
return Response.json({ errors: result.issues }, { status: 400 })
}Reading Query Parameters
Query parameters are part of the URL (e.g. /search?q=remix&page=2). Access them through the url object:
handler({ url }) {
let query = url.searchParams.get('q') // "remix"
let page = url.searchParams.get('page') // "2" (string!)
let tags = url.searchParams.getAll('tag') // ["js", "ts"]
let hasFilter = url.searchParams.has('filter') // true or false
}Query params are always strings
Like route params, query parameters are always strings. Convert them explicitly:
let page = Number(url.searchParams.get('page')) || 1Headers Utilities
Remix provides utilities for working with common headers from remix/headers:
import {
parseAccept,
parseCacheControl,
parseContentType,
} from 'remix/headers'Content Negotiation
Content negotiation is the process of determining what format to respond with based on the client's Accept header:
handler({ request }) {
let accept = request.headers.get('Accept') ?? ''
if (accept.includes('application/json')) {
return Response.json({ books: [...] })
}
return createHtmlResponse('<h1>Books</h1>...')
}Streaming Responses
For large responses, you can stream data using a ReadableStream:
handler() {
let stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('Hello '))
setTimeout(() => {
controller.enqueue(new TextEncoder().encode('World!'))
controller.close()
}, 1000)
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/plain' },
})
}This sends "Hello " immediately and "World!" one second later. The browser receives data as it arrives rather than waiting for the entire response.
Streaming is especially useful for:
- Large HTML pages (see Streaming)
- Server-Sent Events
- File downloads
- Real-time data feeds
Common Response Patterns
Success with Data
return Response.json({ user: { id: 1, name: 'Alice' } })Created
return Response.json(
{ id: newBook.id, message: 'Book created' },
{ status: 201 },
)No Content
return new Response(null, { status: 204 })Bad Request
return Response.json(
{ error: 'Invalid email address' },
{ status: 400 },
)Not Found
return new Response('Page not found', { status: 404 })Redirect After Mutation
import { createRedirectResponse } from 'remix/response/redirect'
return createRedirectResponse(routes.books.index.href())Setting Cookies
let response = new Response('Logged in')
response.headers.set('Set-Cookie', 'token=abc; HttpOnly; Secure')
return responseIn practice, you use the session middleware instead of setting cookies manually. See Sessions & Cookies.
Related
- Routing In Depth --- How requests are matched to handlers.
- Middleware --- The RequestContext and how middleware transforms it.
- Forms & Mutations --- Working with form data.
- Streaming --- Streaming HTML and other responses.
- Error Handling --- HTTP status codes and error responses.