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-fs-routes

v0.4.0

Published

File-system route conventions for Remix 3

Readme

remix-fs-routes

File-system route modules for Remix 3, available as a standalone CLI or plugins for Vite, Rollup, Rolldown, webpack, Rspack, Rsbuild, esbuild, Farm, and Bun.

[!NOTE]

🤖 LLM assistance disclaimer 🤖

This project was developed with the assistance of OpenAI's Sol model. While I have been a rather vocal critic of the social, economic, environmental, and most importantly cognitive impacts of LLMs and companies/people/data centers that power them, and have avoided building anything substantial with them outside of work until now, I wanted to hold an informed opinion by actually using one for something substantial of my own.

Does that make me somewhat hypocritical? Possibly. But it's a weird world. I'm still navigating my way through this mess, and sometimes a hypocrite is a person who's going through change.

Do with that as you will. If you choose not to consume this package because of that, I understand (believe me, I do) and wish you well.

The rest of this readme was produced via LLM. This note, however, is 100% my own words.

Quickstart

Install the package:

pnpm add --save-dev remix-fs-routes

Create an actions module. Its folder name defines the URL:

// app/routes/posts.$slug/actions.ts
import { createAction } from './+route.ts'

export default createAction(({ params }) => {
  return new Response(`Post ${params.slug}`)
})

Add the plugin to your bundler config. For Vite:

// vite.config.ts
import { defineConfig } from 'vite'
import remixFsRoutes from 'remix-fs-routes/vite'

export default defineConfig({
  plugins: [remixFsRoutes()],
})

Register the generated routes with your Remix router:

import { createRouter } from 'remix/router'
import { registerRoutes } from 'virtual:remix-fs-routes/controller'

export const router = createRouter()
registerRoutes(router)

Include the generated declarations in tsconfig.json:

{
  "include": ["app", ".remix-fs-routes/types/**/*"]
}

The plugin generates route companions and refreshes them during bundler watch or development mode. For a bundler-free setup, run remix-fs-routes generate before typechecking or starting the app.

Install

pnpm add --save-dev remix-fs-routes

remix-fs-routes is build-time tooling. Your generated application code depends only on Remix and your own route modules.

Create a route

Each endpoint is an actions module in a direct child folder of app/routes. The folder name defines both the route ID and URL:

app/routes/
  _index/actions.ts
  posts.$slug/actions.ts

The generated +route.ts companion provides a typed createAction factory:

// app/routes/posts.$slug/actions.ts
import { createAction } from './+route.ts'

export default createAction(({ params }) => {
  return new Response(`Post ${params.slug}`)
})

Route middleware is inferred before the handler is typed:

export default createAction({
  middleware: [requireUser],
})(async ({ params, get }) => {
  return new Response(params.slug)
})

Use named exports to handle specific HTTP methods. The default export is the ANY fallback for methods without a named handler:

export let get = createAction(({ params }) => {
  return new Response(`Post ${params.slug}`)
})

export let post = createAction(({ params }) => {
  return new Response(`Created ${params.slug}`, { status: 201 })
})

export default createAction(({ request }) => {
  return new Response(`Handled ${request.method}`)
})

Supported method exports are get, head, post, put, patch, delete, and options. Since delete cannot be used as a JavaScript binding name, export it with an alias:

let remove = createAction(() => new Response(null, { status: 204 }))
export { remove as delete }

HTTP also defines CONNECT and TRACE, but the Fetch Request API and Remix router do not support them. Exporting connect or trace produces a clear error when routes are registered instead of silently ignoring the handler.

The default export is optional. A module with only named method handlers returns the router's normal not-found response for other methods.

Use a bundler plugin

Add the adapter to your bundler config. With Vite:

// vite.config.ts
import { defineConfig } from 'vite'
import remixFsRoutes from 'remix-fs-routes/vite'

export default defineConfig({
  plugins: [remixFsRoutes()],
})

Import the generated registration function when creating your router:

import { createRouter } from 'remix/router'
import { registerRoutes } from 'virtual:remix-fs-routes/controller'

export const router = createRouter()
registerRoutes(router)

Include the generated declarations in your TypeScript project:

{
  "include": ["app", ".remix-fs-routes/types/**/*"]
}

The other adapters use the same API:

| Bundler | Import | | -------- | -------------------------- | | Rollup | remix-fs-routes/rollup | | Rolldown | remix-fs-routes/rolldown | | webpack | remix-fs-routes/webpack | | Rspack | remix-fs-routes/rspack | | Rsbuild | remix-fs-routes/rsbuild | | esbuild | remix-fs-routes/esbuild | | Farm | remix-fs-routes/farm | | Bun | remix-fs-routes/bun |

Rollup sequencing

The Rollup adapter still performs route code generation and virtual-module resolution, but Rollup does not transform TypeScript itself. If you use @rollup/plugin-typescript, generate the route files before starting Rollup so that the TypeScript plugin includes them in its compiler program:

pnpm exec remix-fs-routes generate
pnpm exec rollup --config rollup.config.js

Keep remix-fs-routes/rollup before @rollup/plugin-typescript in the plugin list. Do not rely on the adapter's in-build generation to make newly created .ts files visible to the TypeScript plugin; after adding or removing routes, rerun generate and restart the Rollup process. Other Rollup TypeScript transforms may not have this limitation.

If you want route generation integrated into the bundler lifecycle without this sequencing constraint, prefer the remix-fs-routes/rolldown adapter.

Bundler watch and development modes automatically regenerate routes when files change.

Use the CLI

Generate the route companions, route map, registration function, and virtual-module declarations:

pnpm exec remix-fs-routes generate

Import the physical outputs in your router:

import { createRouter } from 'remix/router'

import { registerRoutes } from './routes.controller.ts'

export const router = createRouter()
registerRoutes(router)

Common commands:

remix-fs-routes generate --watch
remix-fs-routes generate --check
remix-fs-routes typegen

Run generate before standalone TypeScript checks and builds. Use --check in CI to fail when generated files are stale.

Generate URLs

Both the physical and virtual route modules export a typed href() helper keyed by route pattern:

import { href } from './routes.ts'
// or: import { href } from 'virtual:remix-fs-routes/routes'

href('/')
href('/posts/:slug', { slug: 'hello-remix' })
href('/(:lang/)categories', { lang: 'es' })

Route conventions

| Folder | URL | | -------------------- | ---------------------------------- | | _index | / | | about | /about | | about._index | /about/ | | posts.$slug | /posts/:slug | | files.$ | /files/* | | ($lang).categories | /categories, /:lang/categories | | reports.$id[.pdf] | /reports/:id.pdf | | sitemap[.]xml | /sitemap.xml | | _auth.login | /login | | admin_.health | /admin/health | | [_auth].login | /_auth/login |

Route entrypoints may use any JavaScript or TypeScript extension: .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, or .cts. actions modules may export method handlers, a default fallback, or both.

_index as the final segment represents the trailing-slash variant of a URL. A leading underscore makes a segment pathless, and a trailing underscore opts a route out of its matching logical controller boundary. Escape an underscore with brackets when it should be literal.

Logical controller hierarchy

Export a named controller from an actions module to apply middleware to that route and its logical descendants:

app/routes/
  admin/actions.ts
  admin.users/actions.ts
  admin.reports/actions.ts

The generated +route.ts provides both strongly typed factories:

import { requireUser } from '#/middleware/require-user.ts'
import { createAction, createController } from './+route.ts'

export let controller = createController({ middleware: [requireUser] })

export default createAction(({ get }) => {
  let user = get(requireUser)
  return new Response(`Hello ${user.name}`)
})

Nested controller middleware is composed from outermost to innermost. A trailing underscore opts out of the matching boundary: admin.users inherits the controller exported by admin/actions.ts, while admin_.health does not. This hierarchy affects controller organization and request handling only; remix-fs-routes does not provide a nested UI or layout convention.

Configuration

All configuration is optional. The following example shows every option with its default value:

remixFsRoutes({
  cwd: process.cwd(),
  appDirectory: 'app',
  rootDirectory: 'routes',
  ignoredRouteFiles: [],
  routesOutput: 'app/routes.ts',
  controllerOutput: 'app/routes.controller.ts',
  routesExportName: 'routes',
  controllerExportName: 'registerRoutes',
  typegenDirectory: '.remix-fs-routes/types',
})

Equivalent CLI flags are available alongside --routes-export, --controller-export, repeated --ignore flags, and --cwd. Run remix-fs-routes --help for the complete list.

Add generated files to source control ignores:

.remix-fs-routes/
app/routes/**/+route.ts

Programmatic API

import {
  generateRouteArtifacts,
  scanRoutes,
  watchRouteArtifacts,
  writeRouteArtifacts,
} from 'remix-fs-routes'

The raw unplugin factory is available from remix-fs-routes/unplugin for custom integrations.