Skip to main content

Introduction

Remix routing is built on the web Fetch API and provides a minimal, composable system for mapping incoming requests to request handlers and middleware. The router accepts a standard Request and returns a Response, making it portable across all JavaScript runtimes.

Core Concepts

Routes and Route Maps

Routes are organized using route maps - declarative objects that define your entire route structure upfront with type-safe route names and request methods.
import { createRouter } from 'remix/fetch-router'
import { route } from 'remix/fetch-router/routes'

let routes = route({
  home: '/',
  about: '/about',
  blog: {
    index: '/blog',
    show: '/blog/:slug',
  },
})
The route map mirrors the structure of your definitions, with leaves being Route objects:
type Routes = typeof routes
// {
//   home: Route<'ANY', '/'>
//   about: Route<'ANY', '/about'>
//   blog: {
//     index: Route<'ANY', '/blog'>
//     show: Route<'ANY', '/blog/:slug'>
//   },
// }

Creating a Router

Use createRouter() to create a router instance:
import { createRouter } from 'remix/fetch-router'

let router = createRouter({
  // Optional: Global middleware runs on every request
  middleware: [logger()],
  // Optional: Custom 404 handler
  defaultHandler: () => new Response('Not Found', { status: 404 }),
})
options
RouterOptions
Configuration options for the router

Mapping Routes to Handlers

Use router.map() to connect routes to their handlers:
router.map(routes, {
  actions: {
    home() {
      return new Response('Home')
    },
    about() {
      return new Response('About')
    },
    blog: {
      actions: {
        index() {
          return new Response('Blog')
        },
        show({ params }) {
          // params is type-safe with the parameters from the route pattern
          return new Response(`Post ${params.slug}`)
        },
      },
    },
  },
})

Handling Requests

The router uses the standard Fetch API to handle requests:
let response = await router.fetch('https://remix.run/blog/hello-remix')
console.log(await response.text()) // "Post hello-remix"

Method-Specific Routing

Routers provide convenient methods for handling specific HTTP methods:
// GET request
router.get(routes.home, () => {
  return new Response('Home')
})

// POST request
router.post(routes.contact, ({ get }) => {
  let formData = get(FormData)
  let message = formData.get('message')
  return new Response(`Got message: ${message}`)
})

// Other methods: put, patch, delete, head, options
router.put(routes.user, ({ params }) => {
  return new Response(`Update user ${params.id}`)
})

Request Context

Every action and middleware receives a context object with useful properties:
router.get('/posts/:id', ({ request, url, params, method, headers, set, get }) => {
  // request: The original Request object
  console.log(request.method) // "GET"
  
  // url: Parsed URL object
  console.log(url.pathname) // "/posts/123"
  console.log(url.searchParams.get('sort'))
  
  // params: Route parameters (fully typed!)
  console.log(params.id) // "123"
  
  // method: Request method (may differ from request.method with methodOverride)
  console.log(method) // "GET"
  
  // headers: Request headers
  console.log(headers.get('Accept'))
  
  // set/get: Type-safe request-scoped context storage
  let userKey = createContextKey<User>()
  set(userKey, currentUser)
  let user = get(userKey)
  
  return new Response(`Post ${params.id}`)
})
context
RequestContext<params>
The request context object passed to all actions and middleware

Type-Safe Context Keys

Use createContextKey() to create type-safe keys for storing and retrieving values from the request context:
import { createContextKey } from 'remix/fetch-router'

// Create a context key with an optional default value
let userKey = createContextKey<{ id: string; name: string }>()

// In middleware, set the value
function authMiddleware(): Middleware {
  return (context, next) => {
    let user = { id: '123', name: 'Alice' }
    context.set(userKey, user)
    return next()
  }
}

// In actions, get the value (fully typed!)
router.get('/profile', ({ get }) => {
  let user = get(userKey) // { id: string; name: string }
  return Response.json({ user })
})

Router Methods

The Router interface provides these methods:

fetch()

Fetch a response from the router.
let response = await router.fetch('https://api.example.com/posts')
let response = await router.fetch(new Request('https://api.example.com/posts'))
let response = await router.fetch(request)
input
string | URL | Request
required
The request input to fetch
init
RequestInit
The request init options

map()

Map a route or route map to an action or controller.
router.map(routes.home, () => new Response('Home'))
router.map(routes.blog, {
  actions: {
    index() { /* ... */ },
    show({ params }) { /* ... */ },
  },
})
target
MapTarget
required
The route/pattern or route map to match. Can be a string, RoutePattern, Route, or RouteMap.
handler
Action | Controller
required
The action or controller to invoke when the route(s) match.

HTTP Method Helpers

Convenient methods for specific HTTP methods:
  • router.get(route, action) - Map a GET route
  • router.post(route, action) - Map a POST route
  • router.put(route, action) - Map a PUT route
  • router.patch(route, action) - Map a PATCH route
  • router.delete(route, action) - Map a DELETE route
  • router.head(route, action) - Map a HEAD route
  • router.options(route, action) - Map an OPTIONS route
router.get('/users', () => Response.json([]))
router.post('/users', ({ get }) => {
  let formData = get(FormData)
  return Response.json({ created: true })
})
router.delete('/users/:id', ({ params }) => {
  return Response.json({ deleted: params.id })
})

Next Steps

Route Patterns

Learn about URL pattern matching and parameter extraction

Middleware

Add functionality with composable middleware

Controllers

Organize routes with nested controllers

Forms

Handle form submissions with type-safe actions

Build docs developers (and LLMs) love