fetch-router
The fetch-router package is the heart of every Remix application. It creates a router that maps incoming HTTP requests to handler functions using the standard Fetch API Request and Response types.
Installation
# Via the meta-package
npm install remix
# Or individually
npm install @remix-run/fetch-routerImports
// Router, context, and middleware
import {
createRouter,
createContextKey,
RequestContext,
} from 'remix/fetch-router'
// Types
import type {
Router,
RouterOptions,
Middleware,
NextFunction,
Controller,
BuildAction,
RequestHandler,
RequestMethod,
} from 'remix/fetch-router'
// Route helpers
import {
route,
form,
resources,
resource,
get,
post,
put,
patch,
del,
head,
options,
} from 'remix/fetch-router/routes'
// Route helper types
import type {
Route,
RouteMap,
FormOptions,
ResourcesOptions,
ResourcesMethod,
} from 'remix/fetch-router/routes'createRouter(options?)
Creates a new router instance.
import { createRouter } from 'remix/fetch-router'
let router = createRouter()RouterOptions
| Option | Type | Default | Description |
|---|---|---|---|
middleware | Middleware[] | undefined | Global middleware that runs on every request. |
defaultHandler | RequestHandler | 404 response | Handler invoked when no route matches. |
matcher | Matcher<MatchData> | ArrayMatcher | The pattern matcher implementation. |
import { createRouter } from 'remix/fetch-router'
import { logger } from 'remix/logger-middleware'
import { compression } from 'remix/compression-middleware'
let router = createRouter({
middleware: [logger(), compression()],
defaultHandler({ url }) {
return new Response(`Nothing at ${url.pathname}`, { status: 404 })
},
})When middleware is provided as a const tuple, the router's context type automatically reflects the context contributions of each middleware.
Router
The router object returned by createRouter().
router.fetch(input, init?)
Dispatches a request through the router and returns a response. This is the primary entry point --- it has the same signature as the global fetch function.
let response = await router.fetch(request)| Parameter | Type | Description |
|---|---|---|
input | string | URL | Request | The request to handle. |
init | RequestInit | Optional request init options. |
| Returns | Promise<Response> | The response from the matched handler. |
router.get(target, handler)
Registers a handler for GET requests.
router.get('/about', () => {
return new Response('About page')
})router.post(target, handler)
Registers a handler for POST requests.
router.post('/users', ({ request }) => {
// Create a user
return new Response('Created', { status: 201 })
})router.put(target, handler) / router.patch(target, handler) / router.delete(target, handler)
Register handlers for PUT, PATCH, and DELETE requests respectively. Same signature as router.get().
router.head(target, handler) / router.options(target, handler)
Register handlers for HEAD and OPTIONS requests respectively.
router.route(method, target, handler)
Registers a handler for a specific HTTP method and route target. This is the general form that the verb methods delegate to.
router.route('GET', '/health', () => {
return new Response('OK')
})router.map(target, handler)
The most versatile registration method. It accepts either a single route with an action, or a route map with a controller.
Single route:
import { route } from 'remix/fetch-router/routes'
let routes = route({
home: '/',
})
router.map(routes.home, {
handler() {
return new Response('Home')
},
})Route map (controller):
let routes = route({
books: {
index: '/books',
show: '/books/:slug',
},
})
router.map(routes.books, {
actions: {
index() {
return new Response('All books')
},
show({ params }) {
return new Response(`Book: ${params.slug}`)
},
},
})With per-route middleware:
router.map(routes.admin, {
middleware: [requireAuth],
actions: {
index() { /* ... */ },
settings({ params }) { /* ... */ },
},
})RequestContext
Every request handler receives a RequestContext instance. It carries the request, parsed URL, route params, and a type-safe key-value store for middleware data.
Properties
| Property | Type | Description |
|---|---|---|
request | Request | The original Fetch API Request. |
url | URL | The parsed URL of the request. |
params | Record<string, string> | Route parameters extracted from the URL. Typed to the specific route pattern. |
method | RequestMethod | The HTTP method. May differ from request.method when using method-override middleware. |
headers | Headers | A copy of the request headers. |
router | Router | The router handling this request. |
context.get(key)
Retrieves a value from the context's type-safe key-value store.
let user = context.get(UserKey)Throws if the key has no default value and has not been set.
context.set(key, value)
Sets a value in the context's key-value store.
context.set(UserKey, user)context.has(key)
Returns true if a value has been set for the given key.
if (context.has(UserKey)) {
// ...
}createContextKey<T>(defaultValue?)
Creates a type-safe key for storing and retrieving values from RequestContext. This is how middleware passes data to downstream handlers.
import { createContextKey } from 'remix/fetch-router'
interface User {
id: string
name: string
}
let UserKey = createContextKey<User>()With a default value:
let ThemeKey = createContextKey<'light' | 'dark'>('light')When a default value is provided, context.get(ThemeKey) returns the default instead of throwing when no value has been set.
Middleware
A middleware function that can intercept requests, modify context, and optionally short-circuit the handler chain by returning a Response.
import type { Middleware, NextFunction } from 'remix/fetch-router'
import { createContextKey, RequestContext } from 'remix/fetch-router'
let TimingKey = createContextKey<number>()
let timing: Middleware = async (context, next) => {
let start = Date.now()
let response = await next()
let duration = Date.now() - start
response.headers.set('X-Response-Time', `${duration}ms`)
return response
}Signature
interface Middleware<method, params, transform> {
(context: RequestContext, next: NextFunction):
| Response
| undefined
| void
| Promise<Response | undefined | void>
}| Parameter | Type | Description |
|---|---|---|
context | RequestContext | The request context. |
next | NextFunction | Call to invoke the next middleware or handler. Returns Promise<Response>. |
| Returns | Response | void | Promise<...> | Return a Response to short-circuit. Return nothing (or call next()) to continue. |
If a middleware neither returns a Response nor calls next(), the framework automatically calls next() on its behalf.
Context Transforms
Middleware can declare a type-level context transform that tells TypeScript what context entries it adds:
import type { Middleware, ContextEntry } from 'remix/fetch-router'
type AuthTransform = [ContextEntry<typeof UserKey, User>]
let auth: Middleware<'ANY', {}, AuthTransform> = async (context, next) => {
let user = await loadUser(context.request)
context.set(UserKey, user)
return next()
}Downstream handlers then have context.get(UserKey) correctly typed as User.
RequestHandler
The function signature for route handlers.
interface RequestHandler<params, context> {
(context: context): Response | Promise<Response>
}Controller
A type for objects that mirror a route map with matching action handlers. Used with router.map() for route groups.
import type { Controller } from 'remix/fetch-router'
let booksController = {
actions: {
index() {
return new Response('All books')
},
show({ params }) {
return new Response(`Book: ${params.slug}`)
},
},
} satisfies Controller<typeof routes.books>The satisfies Controller<typeof routes.books> check ensures:
- Every route in the group has a corresponding action.
- Each action's
paramstype matches the route's dynamic segments. - No extra actions are defined that do not correspond to routes.
A controller can include middleware that applies to all actions in the group:
let adminController = {
middleware: [requireAuth],
actions: {
index() { /* ... */ },
users({ params }) { /* ... */ },
},
} satisfies Controller<typeof routes.admin>BuildAction
A type helper for building single-route action types from a route or pattern.
import type { BuildAction } from 'remix/fetch-router'
// From a Route object
type HomeAction = BuildAction<'GET', typeof routes.home>
// From a string pattern
type UserAction = BuildAction<'GET', '/users/:id'>Use it to type controller files:
export const showUser: BuildAction<'GET', typeof routes.users.show> = {
handler({ params }) {
// params.id is typed as string
return new Response(`User ${params.id}`)
},
}Route Helpers
Route helpers are imported from remix/fetch-router/routes. They define URL patterns that the router matches against.
route(definitions) / route(prefix, definitions)
Creates a route map from an object. Keys are route names, values are URL patterns or nested objects.
import { route } from 'remix/fetch-router/routes'
let routes = route({
home: '/',
about: '/about',
books: {
index: '/books',
show: '/books/:slug',
},
})With a URL prefix:
let routes = route({
admin: route('admin', {
index: '/', // GET /admin
users: '/users', // GET /admin/users
}),
})Nesting can go as deep as needed:
let routes = route({
api: route('api', {
v1: route('v1', {
users: '/users', // GET /api/v1/users
}),
}),
})form(name, options?)
Creates paired GET/POST routes for pages that display and process a form.
import { route, form } from 'remix/fetch-router/routes'
let routes = route({
contact: form('contact'),
})
// routes.contact.index -> GET /contact
// routes.contact.action -> POST /contactFormOptions
| Option | Type | Default | Description |
|---|---|---|---|
formMethod | string | 'POST' | The HTTP method for the action route. |
names | { index?: string, action?: string } | { index: 'index', action: 'action' } | Custom names for the generated routes. |
let routes = route({
settings: form('settings', { formMethod: 'PUT' }),
})
// routes.settings.index -> GET /settings
// routes.settings.action -> PUT /settingsresources(name, options?)
Generates all seven conventional CRUD routes.
import { route, resources } from 'remix/fetch-router/routes'
let routes = route({
books: resources('books', { param: 'bookId' }),
})| Route | Method | URL Pattern |
|---|---|---|
books.index | GET | /books |
books.new | GET | /books/new |
books.create | POST | /books |
books.show | GET | /books/:bookId |
books.edit | GET | /books/:bookId/edit |
books.update | PUT | /books/:bookId |
books.destroy | DELETE | /books/:bookId |
ResourcesOptions
| Option | Type | Default | Description |
|---|---|---|---|
param | string | 'id' | The name of the dynamic segment. |
only | ResourcesMethod[] | all | Limits which routes are created. Valid values: 'index', 'new', 'create', 'show', 'edit', 'update', 'destroy'. |
exclude | ResourcesMethod[] | [] | Excludes specific routes from the generated set. |
names | Record<ResourcesMethod, string> | default names | Custom names for the generated routes. |
resources('orders', {
only: ['index', 'show'],
param: 'orderId',
})
// Only creates GET /orders and GET /orders/:orderIdget(pattern) / post(pattern) / put(pattern) / del(pattern) / patch(pattern) / head(pattern) / options(pattern)
Creates a route that only responds to a specific HTTP method.
import { get, post, put, del } from 'remix/fetch-router/routes'
let routes = route({
search: get('/search'),
logout: post('/logout'),
updateProfile: put('/profile'),
deleteAccount: del('/account'),
})A plain string like '/about' is equivalent to get('/about').
del() not delete()
The delete helper is named del() because delete is a reserved keyword in JavaScript.
resource(name, options?)
Creates routes for a singular resource (no collection routes). Similar to resources() but without index, new, or create.
Route
A Route object represents a single named route with a method and a pattern. Route objects are created by the route helpers and stored in route maps.
route.href(...args)
Generates a URL string from the route's pattern. TypeScript enforces that you pass the correct parameters.
routes.home.href() // "/"
routes.books.show.href({ slug: 'dune' }) // "/books/dune"For routes with dynamic segments, the params argument is required:
// TypeScript error: Property 'slug' is missing
routes.books.show.href({}) // Error!route.pattern
The underlying RoutePattern instance.
route.method
The HTTP method this route responds to ('GET', 'POST', etc., or 'ANY').
Complete Example
// app/routes.ts
import { route, form, resources, get, post } from 'remix/fetch-router/routes'
export const routes = route({
home: '/',
about: '/about',
contact: form('contact'),
search: get('/search'),
blog: {
index: '/blog',
show: '/blog/:slug',
tag: '/blog/tag/:tag',
},
auth: {
login: form('login'),
logout: post('/logout'),
},
admin: route('admin', {
index: '/',
posts: resources('posts', { param: 'postId' }),
}),
})// app/server.ts
import * as http from 'node:http'
import { createRouter } from 'remix/fetch-router'
import { createRequestListener } from 'remix/node-fetch-server'
import { logger } from 'remix/logger-middleware'
import { routes } from './routes.ts'
import blogController from './controllers/blog.ts'
let router = createRouter({
middleware: [logger()],
})
router.get(routes.home, () => {
return new Response('Welcome to Remix')
})
router.map(routes.blog, blogController)
let server = http.createServer(createRequestListener(router.fetch))
server.listen(3000)// app/controllers/blog.ts
import type { Controller } from 'remix/fetch-router'
import type { routes } from '../routes.ts'
export default {
actions: {
index() {
return new Response('All posts')
},
show({ params }) {
return new Response(`Post: ${params.slug}`)
},
tag({ params }) {
return new Response(`Tag: ${params.tag}`)
},
},
} satisfies Controller<typeof routes.blog>Related Packages
- route-pattern --- The URL pattern matching engine used internally by the router.
- node-fetch-server --- Run the router on Node.js with
http.createServer(). - fetch-proxy --- Forward requests to another server.
Related Guides
- Routing In Depth --- Comprehensive guide to routing concepts.
- Middleware --- How middleware works in the request lifecycle.
- Forms & Mutations --- Handle form submissions with
form()routes.