npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@remix-run/fetch-router

v0.20.1

Published

A minimal, composable router for the web Fetch API

Readme

fetch-router

A minimal, composable router built on the web Fetch API and route-pattern. Use it to define typed route maps, run middleware, and share request-scoped context across APIs, web services, and server-rendered applications.

Features

  • Fetch API: Built on standard web APIs that work everywhere - Node.js, Bun, Deno, Cloudflare Workers, and browsers
  • Type-Safe Routing: Leverage TypeScript for compile-time route validation and parameter inference
  • Typed Request Context: Carry request-scoped context through routers, controllers, and actions
  • Declarative Route Maps: Define your route structure upfront with type-safe route names and request methods
  • Flexible Middleware: Use router, controller, and action middleware for each request boundary
  • Easy Testing: Use standard fetch() to test your routes - no special test harness required

Installation

npm i remix

Usage

The main purpose of the router is to map incoming requests to request handlers and middleware. The router uses the fetch() API to accept a Request and return a Response.

Import route definition helpers (route, form, resource, resources, etc.) from remix/routes and runtime APIs (createRouter, Middleware, etc.) from remix/router.

The example below is a small site with a home page, an "about" page, and a blog.

import { route } from 'remix/routes'
import { createRouter } from 'remix/router'
import { logger } from 'remix/middleware/logger'

// `route()` creates a "route map" that organizes routes by name. The keys
// of the map may be any name, and may be nested to group related routes.
let routes = route({
  home: '/',
  about: '/about',
  blog: {
    index: '/blog',
    show: '/blog/:slug',
  },
})

let router = createRouter({
  // Middleware is used to run code before and/or after actions run.
  // In this case, the `logger()` middleware logs the request to the console.
  middleware: [logger()],
})

// Map a controller that supplies actions for the root routes.
// A controller is a plain object with an `actions` property that
// matches the direct route leaves in a route map.
router.map(routes, {
  actions: {
    home() {
      return new Response('Home')
    },
    about() {
      return new Response('About')
    },
  },
})

// Map another controller that supplies actions for the blog routes.
router.map(routes.blog, {
  actions: {
    index() {
      return new Response('Blog')
    },
    show({ params }) {
      // params is a type-safe object with the parameters from the route pattern
      return new Response(`Post ${params.slug}`)
    },
  },
})

let response = await router.fetch('https://remix.run/blog/hello-remix')
console.log(await response.text()) // "Post hello-remix"

The route map is an object of the same shape as the object pass into route(), including nested objects. The leaves of the map are Route objects, which you can see if you inspect the type of the routes variable in your IDE.

type Routes = typeof routes
// {
//   home: Route<'ANY', '/'>
//   about: Route<'ANY', '/about'>
//   blog: {
//     index: Route<'ANY', '/blog'>
//     show: Route<'ANY', '/blog/:slug'>
//   },
// }

The routes.home route is a Route<'ANY', '/'>, which means it serves any request method (GET, POST, PUT, DELETE, etc.) when the URL path is /. We'll discuss routing based on request method in detail later. But first, let's talk about navigation.

Links and Form Actions

In addition to describing the structure of your routes, route maps also make it easy to generate type-safe links and form actions using the href() function on a route. The example below is a small site with a home page and a "Contact Us" page.

Note: We're using the createHtmlResponse helper from response below to create Responses with Content-Type: text/html. We're also using the html template tag to create safe HTML strings to use in the response body.

import { route } from 'remix/routes'
import { createRouter } from 'remix/router'
import { html } from 'remix/html-template'
import { createHtmlResponse } from 'remix/response/html'

let routes = route({
  home: '/',
  contact: '/contact',
})

let router = createRouter()

// Register an action for `GET /`
router.get(routes.home, () => {
  return createHtmlResponse(`
    <html>
      <body>
        <h1>Home</h1>
        <p>
          <a href="${routes.contact.href()}">Contact Us</a>
        </p>
      </body>
    </html>
  `)
})

// Register an action for `GET /contact`
router.get(routes.contact, () => {
  return createHtmlResponse(`
    <html>
      <body>
        <h1>Contact Us</h1>
        <form method="POST" action="${routes.contact.href()}">
          <div>
            <label for="message">Message</label>
            <input type="text" name="message" />
          </div>
          <button type="submit">Send</button>
        </form>
        <footer>
          <p>
            <a href="${routes.home.href()}">Home</a>
          </p>
        </footer>
      </body>
    </html>
  `)
})

// Register an action for `POST /contact`
router.post(routes.contact, ({ get }) => {
  // POST actions can read parsed FormData from request context using FormData
  // as the context key after the formData middleware has run.
  let formData = get(FormData)
  let message = formData.get('message') as string
  let body = html`
    <html>
      <body>
        <h1>Thanks!</h1>
        <div>
          <p>You said: ${message}</p>
        </div>
        <footer>
          <p>
            <a href="${routes.home.href()}">Home</a>
          </p>
        </footer>
      </body>
    </html>
  `

  return createHtmlResponse(body)
})

Routing Based on Request Method

In the example above, both the home and contact routes are able to be registered for any incoming request.method. If you inspect their types, you'll see:

type HomeRoute = typeof routes.home // Route<'ANY', '/'>
type ContactRoute = typeof routes.contact // Route<'ANY', '/contact'>

We used router.get() and router.post() to register actions on each route specifically for the GET and POST request methods.

However, we can also encode the request method into the route definition itself using the method property on the route. When you include the method in the route definition, router.map() will register the action only for that specific request method. This can be more convenient than using router.get() and router.post() to register actions one at a time.

import * as assert from 'node:assert/strict'
import { createRouter } from 'remix/router'
import { route } from 'remix/routes'

let routes = route({
  home: { method: 'GET', pattern: '/' },
  contact: {
    index: { method: 'GET', pattern: '/contact' },
    action: { method: 'POST', pattern: '/contact' },
  },
})

type Routes = typeof routes
// Each route is now typed with a specific request method.
// {
//   home: Route<'GET', '/'>,
//   contact: {
//     index: Route<'GET', '/contact'>,
//     action: Route<'POST', '/contact'>,
//   },
// }

let router = createRouter()

router.map(routes, {
  actions: {
    home({ method }) {
      assert.equal(method, 'GET')
      return new Response('Home')
    },
  },
})

router.map(routes.contact, {
  actions: {
    index({ method }) {
      assert.equal(method, 'GET')
      return new Response('Contact')
    },
    action({ method }) {
      assert.equal(method, 'POST')
      return new Response('Contact Action')
    },
  },
})

Composing Route Groups

As applications grow, it is useful to let one file own the routes for a specific area of the app while the top-level router decides where that area is mounted. Use router.mount() with a route installer to register a group of routes under a route pattern prefix.

A route installer receives a RouteBuilder, not a full Router, so it can register routes but cannot dispatch requests. The parent router remains the only router that owns request dispatch, the matcher, and the default handler.

import { createRouter, type RouteBuilder } from 'remix/router'
import { get, route } from 'remix/routes'

const adminRouteDefs = {
  index: get('/'),
  users: {
    show: get('/users/:id'),
  },
}

// Use relative route groups inside installers. These routes are registered below
// the mount prefix when `installAdminRoutes()` runs.
const adminRouteGroup = route(adminRouteDefs)

// Use full app routes for links and redirects.
const routes = {
  admin: route('/admin', adminRouteDefs),
}

function installAdminRoutes<context extends AppContext>(router: RouteBuilder<context>) {
  router.map(adminRouteGroup, {
    actions: {
      index() {
        return new Response('Admin')
      },
    },
  })

  router.map(adminRouteGroup.users, {
    actions: {
      show({ params, currentUser }) {
        return new Response(`User ${params.id} for ${currentUser.id}`)
      },
    },
  })
}

let router = createRouter<AppContext>({ middleware })

router.mount('/admin', installAdminRoutes)

Mount prefixes are route patterns. They compose with routes registered inside the installer using joinPatterns(), so params from the mount prefix are available to mounted handlers:

router.mount('/orgs/:orgId', (org) => {
  org.get('/users/:userId', ({ params }) => {
    // params is { orgId: string, userId: string }
    return new Response(`${params.orgId}:${params.userId}`)
  })
})

When a mount prefix and child route use the same param name, the right-most route param wins, matching route-pattern behavior.

Middleware stays on routers, controllers, and actions. If an entire route group needs auth or another boundary, put that middleware on the controllers or actions owned by the installer:

function installAdminRoutes<context extends AppContext>(router: RouteBuilder<context>) {
  router.map(adminRouteGroup, {
    middleware: [requireAdmin()],
    actions: {
      index({ admin }) {
        return new Response(admin.id)
      },
    },
  })
}

Unknown paths below a mounted prefix fall through to the parent router's default handler. If a route group needs its own catch-all response, register one explicitly inside the installer.

Declaring Routes

In addition to the { method, pattern } syntax shown above, the router provides a few shorthand methods that help eliminate some of the boilerplate when building complex route maps:

  • form - creates a route map with an index (GET) and action (POST) route. This is well-suited to showing a standard HTML <form> and handling its submit action at the same URL.
  • resources (and resource) - creates a route map with a set of resource-based routes, useful when defining RESTful API routes or Rails-style resource-based routes.

Declaring Form Routes

Continuing with the example of the contact page, let's use the form shorthand to make the route map a little less verbose.

A form() route map contains two routes: index and action. The index route is a GET route that shows the form, and the action route is a POST route that handles the form submission.

import { createRouter } from 'remix/router'
import { route, form } from 'remix/routes'
import { createHtmlResponse } from 'remix/response/html'
import { html } from 'remix/html-template'

let routes = route({
  home: '/',
  contact: form('contact'),
})

type Routes = typeof routes
// {
//   home: Route<'ANY', '/'>
//   contact: {
//     index: Route<'GET', '/contact'> - Shows the form
//     action: Route<'POST', '/contact'> - Handles the form submission
//   },
// }

let router = createRouter()

router.map(routes, {
  actions: {
    home() {
      return createHtmlResponse(`
        <html>
          <body>
            <h1>Home</h1>
            <footer>
              <p>
                <a href="${routes.contact.index.href()}">Contact Us</a>
              </p>
            </footer>
          </body>
        </html>
      `)
    },
  },
})

router.map(routes.contact, {
  actions: {
    // GET /contact - shows the form
    index() {
      return createHtmlResponse(`
        <html>
          <body>
            <h1>Contact Us</h1>
            <form method="POST" action="${routes.contact.action.href()}">
              <label for="message">Message</label>
              <input type="text" name="message" />
              <button type="submit">Send</button>
            </form>
          </body>
        </html>
      `)
    },
    // POST /contact - handles the form submission
    action({ get }) {
      let formData = get(FormData)
      let message = formData.get('message') as string
      let body = html`
        <html>
          <body>
            <h1>Thanks!</h1>
            <p>You said: ${message}</p>

            <p>
              Got more to say? <a href="${routes.contact.index.href()}">Send another message</a>
            </p>
          </body>
        </html>
      `

      return createHtmlResponse(body)
    },
  },
})

Resource-based Routes

The router provides a resources() helper that creates a route map with a set of resource-based routes, useful when defining RESTful API routes or modeling resources in a web application (similar to Rails' resources helper). You can think of "resources" as a way to define routes for a collection of related resources, like products, books, users, etc.

import { createRouter } from 'remix/router'
import { route, resources } from 'remix/routes'

let routes = route({
  brands: {
    ...resources('brands', { only: ['index', 'show'] }),
    products: resources('brands/:brandId/products', {
      only: ['index', 'show'],
    }),
  },
})

type Routes = typeof routes
// {
//   brands: {
//     index: Route<'GET', '/brands'>
//     show: Route<'GET', '/brands/:id'>
//     products: {
//       index: Route<'GET', '/brands/:brandId/products'>
//       show: Route<'GET', '/brands/:brandId/products/:id'>
//     },
//   },
// }

let router = createRouter()

router.map(routes.brands, {
  actions: {
    // GET /brands
    index() {
      return new Response('Brands Index')
    },
    // GET /brands/:id
    show({ params }) {
      return new Response(`Brand ${params.id}`)
    },
  },
})

router.map(routes.brands.products, {
  actions: {
    // GET /brands/:brandId/products
    index() {
      return new Response('Products Index')
    },
    // GET /brands/:brandId/products/:id
    show({ params }) {
      return new Response(`Brand ${params.brandId}, Product ${params.id}`)
    },
  },
})

The resource() helper creates a route map for a single resource (i.e. not something that is part of a collection). This is useful when defining operations on a singleton resource, like a user profile.

import { createRouter } from 'remix/router'
import { route, resources, resource } from 'remix/routes'

let routes = route({
  user: {
    ...resources('users', { only: ['index', 'show'] }),
    profile: resource('users/:userId/profile', { only: ['show', 'edit', 'update'] }),
  },
})

type Routes = typeof routes
// {
//   user: {
//     index: Route<'GET', '/users'>
//     show: Route<'GET', '/users/:id'>
//     profile: {
//       show: Route<'GET', '/users/:userId/profile'>
//       edit: Route<'GET', '/users/:userId/profile/edit'>
//       update: Route<'PUT', '/users/:userId/profile'>
//     },
//   },
// }

In both of the examples above we used the only option to limit the routes generated by resources()/resource() to only the routes we needed. Without the only option, a resources('users') route map contains 7 routes: index, new, show, create, edit, update, and destroy.

let routes = resources('users')
type Routes = typeof routes
// {
//   index: Route<'GET', '/users'> - Lists all users
//   new: Route<'GET', '/users/new'> - Shows a form to create a new user
//   show: Route<'GET', '/users/:id'> - Shows a single user
//   create: Route<'POST', '/users'> - Creates a new user
//   edit: Route<'GET', '/users/:id/edit'> - Shows a form to edit a user
//   update: Route<'PUT', '/users/:id'> - Updates a user
//   destroy: Route<'DELETE', '/users/:id'> - Deletes a user
// }

Similarly, a resource('profile') route map contains 6 routes: new, show, create, edit, update, and destroy. There is no index route because a resource() represents a singleton resource, not a collection, so there is no collection view.

let routes = resource('profile')
type Routes = typeof routes
// {
//   new: Route<'GET', '/profile/new'> - Shows a form to create the profile
//   show: Route<'GET', '/profile'> - Shows the profile
//   create: Route<'POST', '/profile'> - Creates the profile
//   edit: Route<'GET', '/profile/edit'> - Shows a form to edit the profile
//   update: Route<'PUT', '/profile'> - Updates the profile
//   destroy: Route<'DELETE', '/profile'> - Deletes the profile
// }

Resource route names may be customized using the names option when you'd prefer not to use the default index/new/show/create/edit/update/destroy route names.

import { createRouter } from 'remix/router'
import { route, resources } from 'remix/routes'

let routes = route({
  users: resources('users', {
    only: ['index', 'show'],
    names: { index: 'list', show: 'view' },
  }),
})
type Routes = typeof routes.users
// {
//   list: Route<'GET', '/users'> - Lists all users
//   view: Route<'GET', '/users/:id'> - Shows a single user
// }

If you want to use a param name other than id, you can use the param option.

import { createRouter } from 'remix/router'
import { route, resources } from 'remix/routes'

let routes = route({
  users: resources('users', {
    only: ['index', 'show', 'edit', 'update'],
    param: 'userId',
  }),
})
type Routes = typeof routes.users
// {
//   index: Route<'GET', '/users'> - Lists all users
//   show: Route<'GET', '/users/:userId'> - Shows a single user
//   edit: Route<'GET', '/users/:userId/edit'> - Shows a form to edit a user
//   update: Route<'PUT', '/users/:userId'> - Updates a user
// }

You can use the exclude option to exclude routes from being generated.

let routes = resources('users', { exclude: ['edit', 'update', 'destroy'] })
type Routes = typeof routes
// {
//   index: Route<'GET', '/users'> - Lists all users
//   new: Route<'GET', '/users/new'> - Shows a form to create a new user
//   show: Route<'GET', '/users/:userId'> - Shows a single user
//   create: Route<'POST', '/users'> - Creates a new user
// }

Controllers and Middleware

Middleware functions run code before and/or after actions. They are a powerful way to add functionality to your app.

Every middleware must either return a Response, return the response from next(), or call await next() before it returns. Middleware that returns without calling next() throws at runtime.

A basic logging middleware might look like this:

import { type Middleware } from 'remix/router'

// You can use the `Middleware` type to type middleware functions.
function logger(): Middleware {
  return async (context, next) => {
    let start = new Date()

    // Call next() to invoke the next middleware or action in the chain.
    let response = await next()

    let end = new Date()
    let duration = end.getTime() - start.getTime()

    console.log(`${context.request.method} ${context.request.url} ${response.status} ${duration}ms`)

    return response
  }
}

// Use it like this:
let router = createRouter({
  middleware: [logger()],
})

Middleware is typically built as a function that returns a middleware function. This allows you to pass options to the middleware function if needed. For example, the auth() middleware below allows you to pass a token option that is used to authenticate the request.

interface AuthOptions {
  token: string
}

function auth(options?: AuthOptions): Middleware {
  let token = options?.token ?? 'secret'

  return (context, next) => {
    if (context.headers.get('Authorization') !== `Bearer ${token}`) {
      return new Response('Unauthorized', { status: 401 })
    }
    return next()
  }
}

Middleware can store values in request context with a key. To make that value available as context.db, add property: 'db' to the middleware type and pass { property: 'db' } to context.set():

import { createContextKey, type Middleware } from 'remix/router'

interface Database {
  findMany(): Promise<unknown[]>
}

const Database = createContextKey<Database>()

function loadDatabase(): Middleware<{
  key: typeof Database
  value: Database
  property: 'db'
}> {
  return async (context, next) => {
    context.set(Database, await connectDatabase(), { property: 'db' })
    return next()
  }
}

router.get('/books', async (context) => {
  let books = await context.db.findMany()
  return Response.json(books)
})

Use context.db (or context.get(Database)). If two values use the same property name, the router throws.

Middleware has three API-owned forms: router middleware, controller middleware, and action middleware.

Router middleware is added to the router when it is created using the createRouter({ middleware }) option. This middleware runs before any routes are matched and is useful for doing things like logging, serving static files, profiling, and a variety of other things. Router middleware runs on every request, so it's important to keep it lightweight and fast.

Controller middleware runs for every direct action in a controller. Action middleware runs only for one action, whether that action is created with createAction(), registered in a controller, or registered directly with router.map() or one of the method-specific helpers like router.get(), router.post(), router.put(), router.delete(), etc. The object form for actions is { handler, middleware? }, so you can omit middleware entirely when you do not need it.

A controller's middleware applies only to the direct route actions in that controller, and its actions object may not include nested route-map keys. This is the router's scoped middleware model: map nested route maps explicitly so each controller owns the direct route actions for one route map, and share middleware arrays between controllers that need the same boundary.

let routes = route({
  home: '/',
  admin: {
    dashboard: '/admin/dashboard',
    settings: '/admin/settings',
  },
})

let router = createRouter({
  // Router middleware runs on all requests.
  middleware: [staticFiles('./public')],
})

router.map(routes.home, () => new Response('Home'))

router.map(routes.admin, {
  // Controller middleware applies to all direct actions in this controller.
  middleware: [auth({ token: 'secret' })],
  actions: {
    dashboard() {
      return new Response('Dashboard')
    },
    settings: {
      // Action middleware applies only to this action.
      middleware: [requireAdmin()],
      handler() {
        return new Response('Settings')
      },
    },
  },
})

Request Context

Every action and middleware receives a context object with useful properties:

const UserKey = createContextKey<{ id: string }>()

router.get('/posts/:id', (context) => {
  // request: The original Request object
  console.log(context.request.method) // "GET"
  console.log(context.request.headers.get('Accept'))

  // url: Parsed URL object
  console.log(context.url.pathname) // "/posts/123"
  console.log(context.url.searchParams.get('sort'))

  // params: Route parameters (fully typed!)
  console.log(context.params.id) // "123"

  // set/get: type-safe request-scoped context data on the context object
  context.set(UserKey, currentUser)
  let user = context.get(UserKey)
  if (user == null) throw new Error('Expected current user')
  console.log(user.id)

  return new Response(`Post ${context.params.id}`)
})

Typed Context Contracts

Route params are only half of a handler's type contract. In many apps, handlers also depend on values that middleware loads into request context, like sessions, database connections, or authenticated users.

fetch-router lets you carry that context contract through the router and into direct route registration, stored controllers, and stored actions. A common pattern is to derive one application context type from your middleware, augment RouterTypes.context with it, then use createAction() and createController() to type stored handlers.

import { Auth, requireAuth } from 'remix/middleware/auth'
import { createAction, createController, createRouter, type RouterContext } from 'remix/router'
import { route } from 'remix/routes'
import { loadDatabase } from './middleware/database.ts'
import { loadSession } from './middleware/session.ts'

let routes = route({
  account: '/account',
})

type AuthIdentity = { id: string }

export const router = createRouter({
  middleware: [loadSession(), loadDatabase()],
})

type AppContext = RouterContext<typeof router>

declare module 'remix/router' {
  interface RouterTypes {
    context: AppContext
  }
}

let accountAction = createAction(routes.account, {
  middleware: [requireAuth<AuthIdentity>()],
  handler(context) {
    let auth = context.get(Auth)
    return Response.json({ id: auth.identity.id })
  },
})

let accountController = createController(routes, {
  middleware: [requireAuth<AuthIdentity>()],
  actions: {
    account(context) {
      let auth = context.get(Auth)
      return Response.json({ id: auth.identity.id })
    },
  },
})

In this example, the router's inline middleware array defines the app context contract. RouterContext<typeof router> extracts the request context that the router provides, so RouterTypes.context can use that context without storing the middleware chain separately.

Prefer plain inline arrays for middleware options on routers, controllers, actions, and route helpers. Inline arrays already give TypeScript enough information to infer middleware-provided context for downstream handlers, so createAction() and direct action objects see action middleware context, and createController() sees controller middleware context without createMiddleware().

Use createMiddleware() only when a middleware chain is stored somewhere and its exact tuple type needs to survive that boundary. The common cases are deriving MiddlewareContext<typeof rootMiddleware> without a router value, exporting a reusable chain, or returning a chain from a factory. A standalone array like let middleware = [loadSession(), loadDatabase()] widens to a normal array, so MiddlewareContext<typeof middleware> cannot derive the ordered middleware context.

When manually annotating stored handlers with Action<typeof route, Context> or Controller<typeof routes, Context>, compose any action or controller middleware chain into Context with MiddlewareContext<typeof actionOrControllerMiddleware, AppContext>.

Middleware Provider Guidance

context.get(key) returns a defined value when the context type includes the key or the key has a default. Constructor keys like FormData are useful, but they do not imply presence; use context transforms for required values and handle undefined otherwise.

If you're authoring a middleware package that stores values in request context, treat that context contract as part of the package API. A good provider should usually export:

  • the context key consumers read with context.get(...)
  • the middleware that populates that key at runtime, with a Middleware context transform that describes the value it provides
  • an optional direct context property for values handlers read frequently

Apps can derive request context from the middleware tuple with MiddlewareContext. If they need to describe a context shape without a middleware tuple, they can use the core ContextWithEntry and ContextWithEntries helpers directly.

import {
  createContextKey,
  createMiddleware,
  type Middleware,
  type MiddlewareContext,
} from 'remix/router'

// The context key that consumers will need to read from `context.get(...)`
export const CurrentUser = createContextKey<User | null>()
const currentUserContextProperty = { property: 'currentUser' } as const

// The context effect carried by middleware that sets one context value
export function loadCurrentUser(): Middleware<{
  key: typeof CurrentUser
  value: User | null
  property: 'currentUser'
}> {
  return async (context, next) => {
    context.set(CurrentUser, await getCurrentUser(context.request), currentUserContextProperty)
    return next()
  }
}

let middleware = createMiddleware(loadCurrentUser())
type AppContext = MiddlewareContext<typeof middleware>

// Use context.currentUser (or context.get(CurrentUser)).
// context.currentUser
// context.get(CurrentUser)

Additional Topics

Scaling Your Application

  • how to spread controllers across multiple files

Error Handling and Aborted Requests

  • wrap router.fetch() in a try/catch to handle errors
  • AbortError is thrown when a request is aborted

Content Negotiation

  • use Accept.from() from remix/headers to serve different responses based on the client's Accept header
    • maybe put this on context.accepts() for convenience?

Sessions

  • use a custom sessionStorage implementation to store session data
  • use session.get() and session.set() to get and set session data
  • use session.flash() to set a flash message
  • use session.destroy() to destroy the session

Form Data and File Uploads

  • use the formData() middleware to parse the FormData object from the request body
  • use context.formData or context.get(FormData) to access parsed form data
  • use context.formData.get(name)/getAll(name) to access uploaded files
  • use the uploadHandler option of the formData() middleware to handle file uploads

Request Method Override

  • use the methodOverride() middleware to override the request method
  • use a hidden <input name="_method" value="..."> to override the request method

Response Helpers

Response helpers for creating common HTTP responses are available in the response package:

import { createFileResponse } from 'remix/response/file'
import { createHtmlResponse } from 'remix/response/html'
import { createRedirectResponse } from 'remix/response/redirect'
import { compressResponse } from 'remix/response/compress'

let response = createHtmlResponse('<h1>Hello</h1>')
let response = Response.json({ message: 'Hello' })
let response = createRedirectResponse('/')
let response = compressResponse(uncompressedResponse, request)

See the response documentation for more details.

Working with HTML

For working with HTML strings and safe HTML interpolation, see the html-template package. It provides a html template tag with automatic escaping to prevent XSS vulnerabilities.

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

// Use the template tag to escape unsafe variables in HTML.
let unsafe = '<script>alert(1)</script>'
let response = createHtmlResponse(html`<h1>${unsafe}</h1>`, { status: 400 })

The html.raw template tag can be used to interpolate values without escaping them. This has the same semantics as String.raw but for HTML snippets that have already been escaped or are from trusted sources:

// Use html.raw as a template tag to skip escaping interpolations
let safeHtml = '<b>Bold</b>'
let content = html.raw`<div class="content">${safeHtml}</div>`
let response = createHtmlResponse(content)

// This is particularly useful when building HTML from multiple safe fragments
let header = '<header>Title</header>'
let body = '<main>Content</main>'
let footer = '<footer>Footer</footer>'
let page = html.raw`
  <!DOCTYPE html>
  <html>
    <body>
      ${header}
      ${body}
      ${footer}
    </body>
  </html>
`

// You can nest html.raw inside html to preserve SafeHtml fragments
let icon = html.raw`<svg>...</svg>`
let button = html`<button>${icon} Click me</button>` // icon is not escaped

Warning: Only use html.raw with trusted content. Unlike the regular html template tag, html.raw does not escape its interpolations, which can lead to XSS vulnerabilities if used with untrusted user input.

See the html-template documentation for more details.

Testing

Testing is straightforward because fetch-router uses the standard fetch() API:

import * as assert from 'node:assert/strict'
import { describe, it } from 'node:test'

describe('blog routes', () => {
  it('creates a new post', async () => {
    let response = await router.fetch('https://api.remix.run/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title: 'Hello', content: 'World' }),
    })

    assert.equal(response.status, 201)
    let post = await response.json()
    assert.equal(post.title, 'Hello')
  })

  it('returns 404 for missing posts', async () => {
    let response = await router.fetch('https://api.remix.run/posts/not-found')
    assert.equal(response.status, 404)
  })
})

No special test harness or mocking required! Just use fetch() like you would in production.

Related Packages

Related Work

  • headers - A library for working with HTTP headers
  • form-data-parser - A library for parsing multipart/form-data requests
  • route-pattern - The pattern matching library that powers fetch-router
  • Express - The classic Node.js web framework

License

See LICENSE