• Stars
    star
    659
  • Rank 68,396 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 2 years ago
  • Updated 6 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Remix package to define routes using the flat-routes convention

Remix Flat Routes

All Contributors

This package enables you to define your routes using the flat-routes convention. This is based on the gist by Ryan Florence

βœ¨πŸŽ‰ New in v0.5.0

Integration with Remix Core

Remix flat routes will be a core feature in a future version of Remix. This will be enabled using a config option.

I plan to continue to maintain this package in the future to enable enhancements that will not be in the Remix core version. To simplify maintenance, I expose all enhancements via the options parameter.

Hybrid Routes

You can now use nested folders for your route names, yet still keep the colocation feature of flat routes.

If you have a large app, its not uncommon to have routes nested many levels deep. With default flat routes, the folder name is the entire route path: some.really.long.route.edit/index.tsx

Often you may have several parent layouts like _public or admin. Instead of having to repeat the name in every route, you can create top-level folders, then nest your routes under them. This way you can still take advantage of flat folders with colocation.

Before

❯ tree app/routes-folders
app/routes-folders
β”œβ”€β”€ _index
β”‚   └── page.tsx
β”œβ”€β”€ _public
β”‚   └── _layout.tsx
β”œβ”€β”€ _public.about
β”‚   └── index.tsx
β”œβ”€β”€ _public.contact[.jpg]
β”‚   └── index.tsx
β”œβ”€β”€ test.$
β”‚   β”œβ”€β”€ _route.server.tsx
β”‚   └── _route.tsx
β”œβ”€β”€ users
β”‚   β”œβ”€β”€ _layout.tsx
β”‚   └── users.css
β”œβ”€β”€ users.$userId
β”‚   β”œβ”€β”€ _route.tsx
β”‚   └── avatar.png
β”œβ”€β”€ users.$userId_.edit
β”‚   └── _route.tsx
└── users._index
    └── index.tsx

After

❯ tree app/routes-hybrid
app/routes-hybrid
β”œβ”€β”€ _index
β”‚   └── index.tsx
β”œβ”€β”€ _public
β”‚   β”œβ”€β”€ _layout.tsx
β”‚   β”œβ”€β”€ about
β”‚   β”‚   └── _route.tsx
β”‚   └── contact[.jpg]
β”‚       └── _route.tsx
β”œβ”€β”€ test.$
β”‚   └── _route.tsx
└── users
    β”œβ”€β”€ $userId
    β”‚   β”œβ”€β”€ _route.tsx
    β”‚   └── avatar.png
    β”œβ”€β”€ $userId_.edit
    β”‚   └── _route.tsx
    β”œβ”€β”€ _index
    β”‚   └── index.tsx
    β”œβ”€β”€ _layout.tsx
    └── users.css

Nested folders with flat-files convention (✨ New in v0.5.1)

To create a folder but treat it as flat-file, just append the +to the folder name.

_auth+/forgot-password.tsx => _auth.forgot-password.tsx

NOTE: You can include the _layout.tsx file inside your folder. You do NOT need to have a _public.tsx or users.tsx file.

You can still use flat-folders for colocation. So this is best of both formats.

❯ tree app/routes-hybrid-files/
app/routes-hybrid-files/
β”œβ”€β”€ _auth+
β”‚   β”œβ”€β”€ forgot-password.tsx
β”‚   └── login.tsx
β”œβ”€β”€ _public+
β”‚   β”œβ”€β”€ _layout.tsx
β”‚   β”œβ”€β”€ about.tsx
β”‚   β”œβ”€β”€ contact[.jpg].tsx
β”‚   └── index.tsx
β”œβ”€β”€ project
β”‚   β”œβ”€β”€ _layout.tsx
β”‚   β”œβ”€β”€ parent.child
β”‚   β”‚   └── index.tsx
β”‚   └── parent.child.grandchild
β”‚       β”œβ”€β”€ index.tsx
β”‚       └── styles.css
└── users+
    β”œβ”€β”€ $userId.tsx
    β”œβ”€β”€ $userId_.edit.tsx
    β”œβ”€β”€ _layout.tsx
    └── index.tsx
<Routes>
  <Route file="root.tsx">
    <Route
      path="forgot-password"
      file="routes-hybrid-files/_auth+/forgot-password.tsx"
    />
    <Route path="login" file="routes-hybrid-files/_auth+/login.tsx" />
    <Route file="routes-hybrid-files/_public+/_layout.tsx">
      <Route path="about" file="routes-hybrid-files/_public+/about.tsx" />
      <Route
        path="contact.jpg"
        file="routes-hybrid-files/_public+/contact[.jpg].tsx"
      />
      <Route index file="routes-hybrid-files/_public+/index.tsx" />
    </Route>
    <Route path="project" file="routes-hybrid-files/project/_layout.tsx">
      <Route
        path="parent/child"
        file="routes-hybrid-files/project/parent.child/index.tsx"
      >
        <Route
          path="grandchild"
          file="routes-hybrid-files/project/parent.child.grandchild/index.tsx"
        />
      </Route>
    </Route>
    <Route path="users" file="routes-hybrid-files/users+/_layout.tsx">
      <Route path=":userId" file="routes-hybrid-files/users+/$userId.tsx" />
      <Route
        path=":userId/edit"
        file="routes-hybrid-files/users+/$userId_.edit.tsx"
      />
      <Route index file="routes-hybrid-files/users+/index.tsx" />
    </Route>
  </Route>
</Routes>

Extended Route Filenames

In addition to the standard index | route | page | layout names, any file that has a _ prefix will be treated as the route file. This will make it easier to find a specific route instead of looking through a bunch of index.tsx files. This was inspired by SolidStart "Renaming Index" feature.

So instead of

_public.about/index.tsx
_public.contact/index.tsx
_public.privacy/index.tsx

You can name them

_public.about/_about.tsx
_public.contact/_contact.tsx
_public.privacy/_privacy.tsx

Multiple Route Folders

You can now pass in additional route folders besides the default routes folder. These routes will be merged into a single namespace, so you can have routes in one folder that will use shared routes from another.

Custom Param Prefix

You can override the default param prefix of $. Some shells use the $ prefix for variables, and this can be an issue due to shell expansion. Use any character that is a valid filename, for example: ^

users.^userId.tsx  => users/:userId
test.^.tsx         => test/*

Custom Base Path

You can override the default base path of /. This will prepend your base path to the root path.

Optional Route Segments

React Router will introduce a new feature for optional route segments. To use optional segments in flat routes, simply wrap your route name in ().

parent.(optional).tsx   => parent/optional?

Custom App Directory

You can override the default app directory of app.

πŸ›  Installation

npm install -D remix-flat-routes

βš™οΈ Configuration

Update your remix.config.js file and use the custom routes config option.

const { flatRoutes } = require('remix-flat-routes')

/**
 * @type {import("@remix-run/dev").AppConfig}
 */
module.exports = {
  // ignore all files in routes folder to prevent
  // default remix convention from picking up routes
  ignoredRouteFiles: ['**/*'],
  routes: async defineRoutes => {
    return flatRoutes('routes', defineRoutes)
  },
}

API

function flatRoutes(
  routeDir: string | string[],
  defineRoutes: DefineRoutesFunction,
  options: FlatRoutesOptions,
)

type FlatRoutesOptions = {
  appDir?: string // optional app directory (defaults to app)
  basePath?: string // optional base path (default is '/')
  paramPrefixChar?: string // optional param prefix (default is '$')
  ignoredRouteFiles?: string[] // optional files to ingore as routes (same as Remix config option)
  visitFiles?: VisitFilesFunction // optional visitor (useful for tests to provide files without file system)
}

NOTE: routeDir should be relative to the app folder. If you want to use the routes folder, you will need to update the ignoredRouteFiles property to ignore all files: **/*

πŸ”¨ Flat Routes Convention

Example (flat-files)

routes/
  _auth.forgot-password.tsx
  _auth.login.tsx
  _auth.reset-password.tsx
  _auth.signup.tsx
  _auth.tsx
  _landing.about.tsx
  _landing.index.tsx
  _landing.tsx
  app.calendar.$day.tsx
  app.calendar.index.tsx
  app.calendar.tsx
  app.projects.$id.tsx
  app.projects.tsx
  app.tsx
  app_.projects.$id.roadmap.tsx
  app_.projects.$id.roadmap[.pdf].tsx

As React Router routes:

<Routes>
  <Route element={<Auth />}>
    <Route path="forgot-password" element={<Forgot />} />
    <Route path="login" element={<Login />} />
    <Route path="reset-password" element={<Reset />} />
    <Route path="signup" element={<Signup />} />
  </Route>
  <Route element={<Landing />}>
    <Route path="about" element={<About />} />
    <Route index element={<Index />} />
  </Route>
  <Route path="app" element={<App />}>
    <Route path="calendar" element={<Calendar />}>
      <Route path=":day" element={<Day />} />
      <Route index element={<CalendarIndex />} />
    </Route>
    <Route path="projects" element={<Projects />}>
      <Route path=":id" element={<Project />} />
    </Route>
  </Route>
  <Route path="app/projects/:id/roadmap" element={<Roadmap />} />
  <Route path="app/projects/:id/roadmap.pdf" />
</Routes>

Individual explanations:

filename url nests inside of...
_auth.forgot-password.tsx /forgot-password _auth.tsx
_auth.login.tsx /login _auth.tsx
_auth.reset-password.tsx /reset-password _auth.tsx
_auth.signup.tsx /signup _auth.tsx
_auth.tsx n/a root.tsx
_landing.about.tsx /about _landing.tsx
_landing.index.tsx / _landing.tsx
_landing.tsx n/a root.tsx
app.calendar.$day.tsx /app/calendar/:day app.calendar.tsx
app.calendar.index.tsx /app/calendar app.calendar.tsx
app.projects.$id.tsx /app/projects/:id app.projects.tsx
app.projects.tsx /app/projects app.tsx
app.tsx /app root.tsx
app_.projects.$id.roadmap.tsx /app/projects/:id/roadmap root.tsx
app_.projects.$id.roadmap[.pdf].tsx /app/projects/:id/roadmap.pdf n/a (resource route)

Nested Layouts

Default match

By default, flat-routes will nest the current route into the parent layout that has the longest matching prefix.

Given the layout route app.calendar.tsx, the following routes will be nested under app.calendar.tsx since app.calendar is the longest matching prefix.

  • app.calendar.index.tsx
  • app.calendar.$day.tsx

Override match

Sometimes you want to use a parent layout that is higher up in the route hierarchy. With the default Remix convention, you would use dot (.) notation instead of nested folders. With flat-routes, since routes files always use dots, there is a different convention to specify which layout to nest under.

Let's say you have an app.tsx layout, and you have a route that you don't want to share with the layout, but instead want to match with root.tsx. To override the default parent match, append a trailing underscore (_) to the segment that is the immediate child of the route you want to nest under.

app_.projects.$id.roadmap.tsx will nest under root since there are no matching routes:

  • ❌ app_.projects.$id.tsx
  • ❌ app_.projects.tsx
  • ❌ app_.tsx
  • βœ… root.tsx

Conventions

filename convention behavior
privacy.jsx filename normal route
pages.tos.jsx dot with no layout normal route, . -> /
about.jsx filename with children parent layout route
about.contact.jsx dot child route of layout
about.index.jsx index filename index route of layout
about._index.jsx alias of index.tsx index route of layout*
about_.company.jsx trailing underscore url segment, no layout
app_.projects.$id.roadmap.tsx trailing underscore change default parent layout
_auth.jsx leading underscore layout nesting, no url segment
_auth.login.jsx leading underscore child of pathless layout route
users.$userId.jsx leading $ URL param
docs.$.jsx bare $ splat route
dashboard.route.jsx route suffix optional, ignored completely
investors/[index].jsx brackets escapes conventional characters

NOTE: The underscore prefix for the index route is optional but helps sort the file to the top of the directory listing.

Justification

  • Make it easier to see the routes your app has defined - just pop open "routes/" and they are all right there. Since file systems typically sort folders first, when you have dozens of routes it's hard to see today which folders have layouts and which don't. Now all related routes are sorted together.

  • Decrease refactor/redesign friction - while code editors are pretty good at fixing up imports when you move files around, and Remix has the "~" import alias, it's just generally easier to refactor a code base that doesn't have a bunch of nested folders. Remix will no longer force this.

    Additionally, when redesigning the user interface, it's simpler to adjust the names of files rather than creating/deleting folders and moving routes around to change the way they nest.

  • Help apps migrate to Remix - Existing apps typically don't have a nested route folder structure like today's conventions. Moving to Remix is arduous because you have to deal with all of the imports.

Colocation

While the example is exclusively files, they are really just "import paths". So you could make a folder for a route instead and the index file will be imported, allowing all of a route's modules to live alongside each other. This is the flat-folders convention, as opposed to the flat-files convention detailed above.

Example (flat-folders)

routes/
  _auth.forgot-password.tsx
  _auth.login.tsx
  _auth.tsx
  _landing.about.tsx
  _landing.index.tsx
  _landing.tsx
  app.projects.tsx
  app.projects.$id.tsx
  app.tsx
  app_.projects.$id.roadmap.tsx

Each route becomes a folder with the route name minus the file extension. The route file then is named index.tsx.

So app.projects.tsx becomes app.projects/index.tsx

routes/
  _auth/
    index.tsx x <- route file (same as _auth.tsx)
  _auth.forgot-password/
    index.tsx  <- route file (same as _auth.forgot-password.tsx)
  _auth.login/
    index.tsx   <- route files (same as _auth.login.tsx)
  _landing.about/
    index.tsx   <- route file (same as _landing.about.tsx)
    employee-profile-card.tsx
    get-employee-data.server.tsx
    team-photo.jpg
  _landing.index/
    index.tsx   <- route file (same as _landing.index.tsx)
    scroll-experience.tsx
  _landing/
    index.tsx   <- route file (same as _landing.tsx)
    header.tsx
    footer.tsx
  app/
    index.tsx   <- route file (same as app.tsx)
    primary-nav.tsx
    footer.tsx
  app_.projects.$id.roadmap/
    index.tsx   <- route file (same as app_.projects.$id.roadmap.tsx)
    chart.tsx
    update-timeline.server.tsx
  app.projects/
    index.tsx <- layout file (sames as app.projects.tsx)
    project-card.tsx
    get-projects.server.tsx
    project-buttons.tsx
  app.projects.$id/
    index.tsx  <- route file (sames as app.projects.$id.tsx)

Aliases

Since the route file is now named index.tsx and you can colocate additional files in the same route folder, the index.tsx file may get lost in the list of files. You can also use the following aliases for index.tsx. The underscore prefix will sort the file to the top of the directory listing.

  • _index.tsx
  • _layout.tsx
  • _route.tsx

NOTE: The _layout.tsx and _route.tsx files are simply more explicit about their role. They work the same as index.tsx.

As with flat files, an index route (not to be confused with index route file), can also use the underscore prefix. The route _landing.index can be saved as _landing.index/index.tsx or _landing._index/_index.tsx.

This is a bit more opinionated, but I think it's ultimately what most developers would prefer. Each route becomes its own "mini app" with all of its dependencies together. With the ignoredRouteFiles option it's completely unclear which files are routes and which aren't.

🚚 Migrating Existing Routes

You can now migrate your existing routes to the new flat-routes convention. Simply run:

npx migrate-flat-routes <sourceDir> <targetDir> [options]

Example:
  npx migrate-flat-routes ./app/routes ./app/flatroutes --convention=flat-folders

NOTE:
  sourceDir and targetDir are relative to project root

Options:
  --convention=<convention>
    The convention to use when migrating.
      flat-files - Migrates all files to a flat directory structure.
      flat-folders - Migrates all files to a flat directory structure, but
        creates folders for each route.

😍 Contributors

Thanks goes to these wonderful people (emoji key):


Kiliman

πŸ’» πŸ“–

Ryan Florence

πŸ“–

Brandon Pittman

πŸ“– πŸ’»

Mehdi Achour

πŸ“–

Fidel GonzΓ‘lez

πŸ“–

Andrew Haines

πŸ’»

Wonu Lee

πŸ’»

Markus Wolf

πŸ’»

This project follows the all-contributors specification. Contributions of any kind welcome!

More Repositories

1

operator-mono-lig

Add ligatures to Operator Mono similar to Fira Code
JavaScript
3,191
star
2

tailwindui-crawler

tailwindui-crawler downloads the component HTML files locally
JavaScript
758
star
3

remix-typedjson

This package is a replacement for superjson to use in your Remix app. It handles a subset of types that `superjson` supports, but is faster and smaller.
TypeScript
432
star
4

remix-params-helper

This package makes it simple to use Zod with standard URLSearchParams and FormData which are typically used in Remix apps.
TypeScript
238
star
5

rmx-cli

A CLI tool for Remix applications
TypeScript
178
star
6

remix-vite-template

Remix template with Vite, Tailwind CSS, and Fly.io support
TypeScript
134
star
7

shadcn-custom-theme

This tool generate a custom theme similar to the ones created by the [shadcn/ui Themes website](https://ui.shadcn.com/themes). You can specify the primary, secondary, accent, and gray colors. The color name should be one of the default Tailwind color names (e.g. red, green, blue, indigo, etc.)
JavaScript
122
star
8

remix-hydration-fix

Sample app that shows how to fix React hydration issues in a Remix app
TypeScript
112
star
9

remix-express-vite-plugin

This package includes a Vite plugin to use in your Remix app. It configures an Express server for both development and production using TypeScript.
TypeScript
101
star
10

remix-component-data

This is a proof of concept for showing how you can expose loader functions from your components to use in your routes.
TypeScript
81
star
11

remix-superjson

Sample showing how to use superjson in your Remix app and full fidelity types with inference
TypeScript
39
star
12

remix-mount-routes

Package for mounting Remix app to non-root routes
TypeScript
37
star
13

remix-scoped-params

Project showing how to track params across multiple routes by scope
TypeScript
36
star
14

remix-global-data

Example showing how to use global data in your Remix app
TypeScript
35
star
15

remix-ecommerce

Remix sample wth product catalog and shopping cart
TypeScript
32
star
16

remix-error-logging

Patch to add server side error logging support to Remix
TypeScript
23
star
17

remix-single-fetch

Remix example showing how to use new Single Data Fetch feature
TypeScript
21
star
18

remix-esbuild-analysis

Includes patch to add --metafile and bundle anaysis support to Remix compiler
15
star
19

remix-workshop

Projects used for the Remix Conf Europe Workshop
TypeScript
15
star
20

epic-stack-with-svg-sprites

Epic Stack example that shows how to use SVG sprites for your React icons
TypeScript
15
star
21

remix-server-folders

Patch to add .server extension to folder in Remix
TypeScript
14
star
22

boron-files

This app is inspired by the Carbon source-code image generator. It enables you to create and share beautiful images of your file list.
TypeScript
13
star
23

remix-vite-express

TypeScript
13
star
24

express-auth-example

TypeScript
13
star
25

epic-stack-time-zone

Epic Stack example with Time Zone client hint
TypeScript
12
star
26

kiliman-dev

TypeScript
12
star
27

remix-component-errorboundary

Example showing how to use `ComponentErrorBoundary`
TypeScript
10
star
28

strip-sourcemaps

CLI to strip server-code from sourcemaps
JavaScript
10
star
29

remix-fastify-app

Test app showing Fastify and Remix integration
JavaScript
10
star
30

remix-walletconnect

Remix example showing how to use WalletConnect with Remix
TypeScript
9
star
31

remix-suspense

Remix example showing how to ensure the Suspense fallback is rendered on route change
TypeScript
9
star
32

remix-blog-mongodb

Remix example that updates the blog tutorial to use MongoDB and Quill
JavaScript
9
star
33

remix-ras-server-example

Sample Remix App Server with custom server file
TypeScript
8
star
34

mono-symbolicate-helper

Helper service to convert obfuscated stack traces to meaningful ones using mono-symbolicate
C#
8
star
35

remix-build-error

A sample app with patches to display build errors in the browser
TypeScript
8
star
36

MvxCommandToMessage

Sample showing how to use a value converter to convert a command into a message
C#
6
star
37

remix-vite-mui

Remix+VIte+MUI example
TypeScript
6
star
38

remix-playground

TypeScript
6
star
39

remix-streams

TypeScript
4
star
40

remix-ftp-deploy

Example to show how to bundle and FTP your Remix app to a host that doesn't let you build
TypeScript
4
star
41

remix-json-fetcher

TypeScript
4
star
42

NuForVS

Nu for Visual Studio
C#
4
star
43

mvx-samples

Some sample apps for MvvmCross
C#
3
star
44

remix-upload

TypeScript
3
star
45

remix-typedjson-example

TypeScript
3
star
46

remix-flat-routes-example

Example site showing the Remix flat routes routing convention
TypeScript
2
star
47

remix-1.15.0-vercel

Remix v1.15.0 Vercel Template
TypeScript
2
star
48

remix-root-errorboundary

Remix example showing root ErrorBoundary
TypeScript
2
star
49

MicrowDB

A mini PCL version of RavenDB client for mobile. Uses SQLite.
C#
2
star
50

epic-stack-theme

Epic Stack example showing how to customize the theme using shadcn-custom-theme tool
TypeScript
2
star
51

remix-js-cookie

Sample showing how to use client-side cookies in Remix
TypeScript
2
star
52

remix-vite-2.2.0

Sample repo using unstable Vite support in Remix
TypeScript
2
star
53

remix-drawer

Example app showing Drawer navigation UI
TypeScript
2
star
54

remix-mount-routes-example

Example showing remix app mounted to non-root URL
TypeScript
1
star
55

epic-stack-kiliman

1
star
56

remix-indie-typedjson

TypeScript
1
star
57

MvxEventsSample

Sample on how to use MvvmCross MvxMessenger plugin with typed Events class
C#
1
star
58

remix-vscode-themes

1
star
59

remix-dnd

JavaScript
1
star
60

remix-docker-test

JavaScript
1
star
61

remix-vite-params

Test repo using Vite and remix-params-helper
TypeScript
1
star
62

remix-vite-2.4.1-pre.2

JavaScript
1
star
63

remix-mapbox

TypeScript
1
star
64

remix-vite-basic

Basic Remix+Vite template with React Canary
TypeScript
1
star
65

remix-custom-domain

Remix example showing how to map custom domain to route param
TypeScript
1
star