Skip to content

Instantly share code, notes, and snippets.

@jamiebuilds
Last active November 29, 2023 06:04
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamiebuilds/86d467ee4353cb316edce8e69ad19237 to your computer and use it in GitHub Desktop.
Save jamiebuilds/86d467ee4353cb316edce8e69ad19237 to your computer and use it in GitHub Desktop.

Idea: Flat file system for file-based routing

Personally I've never liked how tools like Remix or NextJS have mapped a nested file system to routes. Simple things like "I want to put this component in its own file" become annoying tasks.

I've always been a fan of "flatter" file systems, my files often look like this:

/App/
    AppLayout.tsx
    AppLayoutNav.tsx
/AppOnboarding/
    AppOnboardingLayout.tsx
    AppOnboardingTour.tsx
/AppOnboardingFirstSite/
    AppOnboardingFirstSiteRoute.tsx
/AppOnboardingSetupLocal/
    AppOnboardingSetupLocalRoute.tsx
...

This admitably takes a bit to get used to, but it's great for focusing on one part of the app at a time, and seeing all the different parts of your app in one flat directory makes discovery easier.

However, conventions are good, and using the file system for routing does have value. So I'm proposing a new way to do it:

Flat File System Routing

Instead of a nested file system for declaring routes, all of the routes are declared in a flat file structure.

Conventions

Dots . get mapped to / in the route:

a.b.c.d.e -> /a/b/c/d/e

Leading underscores _ ignore the segment in the route:

a._b.c._d.e -> /a/c/e

Leading dollar sign $ becomes a dynamic segment:

a.$b.c.$d.e -> /a/:b/c/:d/e

Trailing dollar sign $ becomes a splat:

a.b.c.d.e$ -> /a/b/c/d/*e

Trailing dots . marks the route as a layout route:

a.   -> (layout: a.*)
a.b  -> /a/b (renders inside a.)

Trailing underscores _ ignore the layout:

a.    -> (layout: a.*)
a_.b  -> /a/b (does not render inside a.)

Example

/pages/
    /_landing./                  -> (layout: _landing.*)
    /_landing.index/             -> /
    /_landing.company/           -> /company
    /_landing.company.team/      -> /company/team
    /_landing.company.careers/   -> /company/careers
    /_landing.docs.doc$/         -> /docs/*
    /_auth./                     -> (layout: _auth.*)
    /_auth.login/                -> /login
    /_auth.signup/               -> /signup
    /_auth.forgot-password/      -> /forgot-password
    /_auth.reset-password/       -> /reset-password
    /app_.loading/               -> /app/loading (does not use app.* layout)
    /app./                       -> (layout: app.*)
    /app.onboarding./            -> (layout: app.onboarding.*)
    /app.onboarding.first-site/  -> /app/onboarding/first-site
    /app.onboarding.setup-local/ -> /app/onboarding/setup-local
    /app.sites.$site/            -> /app/sites/:site
    ...

Files

Each route directory must contain exactly one *.route.* file at its root, but otherwise does not care what files you put inside:

/pages/
    /_landing./
        Landing.route.tsx (layout route)
        Landing.css
        LandingLogo.svg
        LandingNav.tsx
        LandingFooter.tsx
    /_landing.index/
        LandingIndex.route.tsx (page route)
    /_landing.company.index/
        LandingCompanyIndex.route.tsx (page route)
    ...

In Remix, that same app would look something like this:

/app/
  /styles/
      landing.css
  /components/
      /landing/
          LandingNav.tsx
          LandingFooter.tsx   
  /pages/
      __landing.tsx (layout route)
      /__landing/
          index.tsx (page route)
          company.tsx (page route)
      ...
/public/
    /images/
        landing-logo.svg

When you have a few hundred or thousand files, this only gets more and more chaotic, while the flat file system "scales" linearly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment