Ishan Manandhar Ishan is a passionate product designer and frontend developer. He likes learning and implementing new tech stacks. He frequently writes blogs and also runs his YouTube channel, For Those Who Code.

Remix flat routes: An evolution in routing

6 min read 1882 105

Remix flat routes: An evolution in routing

Routing is a vital part of building your application. It can touch on some of the most sensitive parts of your application like authentication, data fetching, code bundling, the initial request, the compiler, and almost every interaction your users take to navigate your application. At its core, most applications have API endpoints that can also be considered routing mechanisms that fuel your entire application.

Remix is a full-stack React framework for server-side-rendered applications that is heavily based on the concept of routing, thanks to its foundation built on top of React Router.

In this post, we’ll be looking at Remix’s unique routing system in order to understand it and its concept of flat routes more thoroughly.

Jump ahead:

Understanding routing in Remix

Remix contains and makes use of many types of routing principles, approaches, and patterns we can choose from to navigate into our application. Below are some of the built-in routing options available in Remix:

  • Route, or Route Module
  • URL
  • Nested Routes
  • Path, or Route Path
  • Parent Layout Route
  • Pathless Route
  • Child Route
  • Index Route
  • Dynamic Segment
  • Splat
  • Outlet

You’ll notice flat routes isn’t mentioned — that’s because it’s a third-party package. The official GitHub repository offers several ways to add this routing approach to our Remix apps today.

Let’s take a look at how to use this library, which was built and is maintained to match Remix’s routing conventions, and use flat routes in an existing Remix application.

Filenaming conventions in Remix

There are some filename conventions we need to understand before we implement flat routes in our application.

Use a single underscore instead of a double underscore prefix, as stated below:

_ prefix  (for pathless route)

The params prefix remains unchanged:

$ params prefix

Use . instead of / to use URL segments:

. path separator

This is not a layout, as it has a double underscore suffix:

_suffix (not a layout no Outlet)

Implementing flat routes in Remix

As previously stated, Remix flat routes will be a core feature in a future version of Remix, but today we can enable flat routes via a third-party package using a config option.

There are two approaches to configuring flat routes: flat files and flat folders.

Flat files convention

Flat files are suited for simpler applications with smaller numbers of files and folders — there will be no folders, in this case. Our entire application is comprised of files:

📂 our-app/routes
┃ 📜_public.tsx
┃ 📜_public.about.tsx (/about) 
┃ 📜_public.feature.tsx  (/feature)
┃ 📜 index.tsx (/)
┃ 📜 author.tsx 
┃ 📜 author._index.tsx  (/author/)
┃ 📜 author.$authorId.tsx (/author/1234)
┃ 📜 author.$authorId_.edit.tsx (/author/1234/edit)

As you can see, we don’t really have folders in our application besides the one containing our entire app.

You may have noticed the author prefix at the beginning of some filenames. This structure acts as a parent layout for our application. This should match the longest matching prefix, which basically determines which is a parent layout is. In contrast to other filenaming conventions, author._index.tsx becomes our home route, instead of author.tsx.We also set the index of the author by placing a underscore after a period in author._index.tsx.

We can describe the file as not a being parent layout using an underscore (_), as seen in the /author/1234/edit path in the above file tree.

Limitations with flat files

There are some limitations with flat files. If there are no folders, we cannot co-locate supporting files with this type of route mechanism — it’s just not suitable for larger applications that serve hundred of routes.

Flat folders convention

Flat folders are well-suited for large apps where there are lots of files and folders to organize when assembling an application.

The main difference between flat folders and flat files is that instead of routing using filenames, the folder itself is treated as the route name. Take a look at the below flat folder architecture:

📂 our-app/routes
┃ ┣ 📂 index
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📂 _public
┃ ┣ ┗ 📜layout.tsx
┃ ┣ 📂 _public.about
┃ ┣  ┗ 📜index.tsx (/about) 
┃ ┣ 📂 _public.feature
┃ ┣  ┗ 📜index.tsx  (/feature)
┃ ┣ 📂 author
┃ ┃ ┗ 📜layout.tsx
┃ ┣ 📂 author._index
┃ ┣  ┗ 📜index.tsx  (/author)
┃ ┣ 📂 author.$authorId
┃ ┣  ┗ 📜 index.tsx (/author/1234)
┃ ┣ 📂 author.$authorId_.edit
┃ ┣  ┗ 📜index.tsx (/author/1234/edit)

Internally, Remix does not differentiate between routes and layouts: layouts are routes with outlets. So here, the files inside of the folders — e.g., index and author can have files named index or layout, which happen to be the same thing in most cases. They need to be more descriptive, which is why we add aliases to our filenames.

Also of note, the underscore prefix (_) will sort routes and layouts of the files to the top of the file list during co-location.

Here are some handy aliases used in naming Remix route file names:

  • index.tsx_index.tsx
  • route.tsx_route.tsx
  • Layout.tsx_layout.tsx
  • page.tsx_page.tsx

How to use flat routes in a demo application

Let’s implement our flat routes in a Remix project. We will build a route structure for an application where users can see the landing page first, authenticate, and enter the main dashboard, where they can visit the taskList route on the dashboard, visit the dynamic ID path for a single task, and visit the root dashboard page itself.

Create a new Remix project and install our flat routes package as dependency there. Initialize the app:

npx remix-create@latest
cd our-project
npm i -D remix-flat-routes

After these commands, change the main remix.config.ts file to use the flat routes package. After these changes, our config file looks something like this:

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)
 // When running locally in development mode, we use the built-in remix
 // server. This does not understand the vercel lambda module format,
 // so we default back to the standard build output.
 server: process.env.NODE_ENV === "development" ? undefined : "./server.ts",
 serverBuildPath: "api/index.js",
 // appDirectory: "app",
 // assetsBuildDirectory: "public/build",
 // publicPath: "/build/",
 serverModuleFormat: "cjs",
 future: {
   v2_dev: true,
   v2_errorBoundary: true,
   v2_headers: true,
   v2_meta: true,
   v2_normalizeFormMethod: true,
   v2_routeConvention: true,

Now, we can start creating files and folders inside of our routes folder. Let’s take a case of flat files as an example.

Below are the files and folders we have created so far:

📂 routes
┃ 📜 _auth.forgot-password.tsx
┃ 📜 _auth.login.tsx 
┃ 📜 _auth.signup.tsx
┃ 📜 _landing.about.tsx
┃ 📜 _landing.index.tsx
┃ 📜 app.tsx
┃ 📜 app_.tasklist.$id.tsx
┃ 📜 app_.tasklist.index.tsx
┃ 📜 landing.tsx  
┃ 📜 auth.tsx

And here is a table that tracks where each filename directs each user:

Filename URL served
_auth.forgot-password.tsx /forgot-password
_auth.login.tsx /login
_auth.signup.tsx /signup
_landing.about.tsx /about
_landing.index.tsx /
app.tsx N/A
app_.tasklist.$id.tsx app/tasklist/:id
app_.tasklist.index.tsx app/tasklist
landing.tsx /landing</td>
auth.tsx /auth

Adding hybrid routing

Let’s introduce the concept of hybrid routing, which helps us use a nested folder structure for our application where we can preserve the co-location feature of flat routes.

In a large application, we may need some parent layouts like _public, _auth, or users. With hybrid routing, we can create a top-level folder for each and nest our routes under them. We can also append + to a folder name to create a folder but treat it as a flat file.

Below are the files, folders, and naming standards we will create:

📂 hybrid-routing/app/routes

┃ ┣ 📂 _index
┃ ┃ ┗ 📜_index.tsx
┃ ┣ 📂 _public
┃ ┃ ┗ 📜_layout.tsx
┃ ┃ ┗ 📂 pricing
┃ ┃ ┃ ┗ 📜_index.tsx
┃ ┃ ┗ 📂 about
┃ ┃ ┃ ┗ 📜_index.tsx
┃ ┃ ┗ 📂 tasklist+
┃ ┃ ┃ ┗ 📜$taskId.tsx
┃ ┃ ┃ ┗ 📜$taskId_.create.tsx
┃ ┃ ┃ ┗ 📜$taskId_.edit.tsx
┃ ┃ ┃ ┗ 📜_index.tsx
┃ ┃ ┗ 📂 users
┃ ┃ ┃ ┗ 📂 $userId
┃ ┃ ┃ ┃ ┗ 📜_route.tsx
┃ ┃ ┃ ┗ 📂 $userId_.create
┃ ┃ ┃ ┃ ┗ 📜_route.tsx
┃ ┃ ┃ ┗ 📂 $userId_.edit
┃ ┃ ┃ ┃ ┗ 📜_route.tsx
┃ 📜_public/layout.tsx

Now, in the terminal, let’s visualize things clearly with the following command:

npx remix routes

We should be able to see the same routing style here as we would in JSX if we were working with a React app:

   <Route file="root.tsx">
       <Route index file="routes/_index/_index.tsx" />
       <Route file="routes/_public/_layout.tsx">
           <Route path="about" file="routes/_public/about/_index.tsx" />
           <Route path="pricing" file="routes/_public/pricing/_index.tsx" />
           <Route path="layout" file="routes/_public.layout.tsx" />
       <Route path="another" file="routes/another/_route.tsx" />
       <Route path="tasklist/:taskId" file="routes/tasklist+/$taskId.tsx" />
       <Route path="tasklist/:taskId/create" file="routes/tasklist+/$taskId_.create.tsx" />
       <Route path="tasklist/:taskId/edit" file="routes/tasklist+/$taskId_.edit.tsx" />
       <Route path="tasklist/" index file="routes/tasklist+/index.tsx" />
       <Route path="users/:userId" file="routes/users/$userId/_route.tsx" />
       <Route path="users/:userId/create" file="routes/users/$userId_.create/_route.tsx" />
       <Route path="users/:userId/edit" file="routes/users/$userId_.edit/_route.tsx" />
       <Route path="users/" index file="routes/users/_index/index.tsx" />

We can clearly see the paths that have been generated for us where we used hybrid routing alongside the flat files convention. In this way, we can use the remix-flat-routes package to create a more flexible and modern routing structure for our entire application.

Benefits of using flat routes

Despite the breadth of inbuilt route options in Remix, the flat route package is really handy. Here are some of the advantages of using this package:

  • Makes visualizing your routes easy, as you can see how you designed it
  • You can co-locate supported files with each route
  • Decreases friction during refactor/redesign
  • Helps us to migrate apps to Remix
  • Actively maintained and planned as part of a future Remix release

Migrating existing routes to flat routes

In addition to building a new app using flat routes, we can use the library to migrate an existing application to use the flat route convention.

Start by using this command, per the library documentation:

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

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

  sourceDir and targetDir are relative to project root

    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.

We can specify whether we want to migrate our existing app to a flat files or flat folders convention using the above command. Using Remix flat routes should generate the same output as a normal Remix router. We can verify this by running:

npx remix routes


This new approach to routing allows us to build a routing system for fully dynamic applications. We can easily implement and use flat routes inside our existing Remix applications. It ultimately simplifies the existing convention as well as gives you new capabilities, as it’s likely that flat routes will be a default routing option in future versions of Remix.

Get set up with LogRocket's modern error tracking in minutes:

  1. Visit to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    Add to your HTML:

    <script src=""></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
Ishan Manandhar Ishan is a passionate product designer and frontend developer. He likes learning and implementing new tech stacks. He frequently writes blogs and also runs his YouTube channel, For Those Who Code.

Leave a Reply