Colocate your routes into feature folders with Remix Custom Routes
There are two main schools of thought when it comes to organizing code in a project.
One method is to organize by type or category. For example, all of your components go in a components
folder, all of your pages go in a pages
folder, and so on.
This was popularized by many standard patterns, like MVC, MVVC, ECS, and so on. But there are many problems with this approach.
Each feature ends up spread throughout the project. For example, if you have a User
feature, you might have many User
components, a User
page, a User
store, and so on. This makes it difficult to find all of the code related to a feature.
A component that is only used in one file still gets put into the components folder. It's easy to end up with dead code when it stops being used but someone forgets to delete it. You're never quite sure if it's safe to delete a file, or even to make changes to one, because it could be used somewhere else.
This architecture encourages the reuse of code, even when it's not wholly appropriate to do so. Rather than creating a new component for the new use-case, a developer might grab an existing component and try to modify it to fit their needs. This leads to spaghetti-like dependencies between unrelated sections of the codebase.
The case for colocation
If a function or component is only ever used once, does it even need to be in a separate file?
When you need a variable or function, write it inline right where you need it. Code readability improves because everything that's relevant is available at a glance.
If you need to use it in more than one place, you can lift it out into a common parent. That might be in the same file, or it might be in a new file. But you don't need to worry about that until you actually need it.
Code editors usually have built in refactor tools that make it easy to extract a function or component into a new file. It'll move to a parent folder and update all of the references for you automatically.
This colocation-first approach to development solves many of the problems with the categorical structure.
- It's easy to find all of the code related to a feature because it's all nearby in the codebase.
- Code that is no longer used is usually spotted by the linter immediately with warnings.
- Imports from sibling files or random other folders can be treated with suspicion during code review, keeping the import graph (and your mental model of the code structure) simple.
Following this process, you'll naturally end up with a structure that looks like domain-driven design or feature folders, where all of the code related to a feature is in a single folder.
Folder based routing
The first version of Remix used a folder-based file structure for routing. Every file in the routes
folder would be a route in your app, and it would resemble the URL structure of your site.
This structure is fairly intuitive for early development, but it has some issues at scale. You can end up with deeply nested routes, and sharing code between routes has to be done outside the routes
folder.
Sometimes related routes need different page layouts, and this convention separates them in the file tree.
Components that have been extracted from a route all end up mixed together. It's not clear which components are related to which routes.
app/├── root.tsx├── cache.server.ts├── db.server.ts│├── components/│ ├── authorCard.tsx│ ├── blogList.tsx│ ├── blogPost.tsx│ ├── footNote.tsx│ ├── otpInput.tsx│ ├── paginationButtons.tsx│ ├── todoContainer.tsx│ ├── todoItem.tsx│ └── todoList.tsx│├── content/│ ├── goodnight-moon.md│ └── hello-world.md│├── routes/│ ├── _layout.tsx # layout route for most of the app│ ├── _layout/│ │ ├── about.tsx│ │ ├── blog/│ │ │ ├── _post/│ │ │ │ ├── $slug.tsx│ │ │ │ └── todo-app.md # related code has no layout│ │ │ ├── _post.tsx│ │ │ └── index.tsx│ │ └── contact.tsx│ ││ ├── auth/│ │ ├── create-account.tsx # /auth/create-account│ │ ├── login.tsx # /auth/login│ │ ├── login/│ │ │ └── otp.tsx # /auth/login/otp│ │ └── reset-password.tsx # /auth/reset-password│ ││ ├── blog/ # blog routes no layout│ │ ├── $slug/│ │ │ └── refresh.ts # /blog/hello-world/refresh│ │ └── todo-app/│ │ └── example.tsx│ └── seed.json├── session.server.ts├── seed.json # not a route, just an asset└── sendgrid.server.ts
This convention is no longer included in Remix by default, but is still available as a separate routing package
Flat file routing
In v2, Remix switched its default convention to a flat file structure. Every route is a top level filename or folder name in the /routes
directory.
If you have a folder, the index.ts
inside becomes the route file, and you're free to include other files in the folder as you see fit.
This is a big win for colocation. Any assets or components related to a route can be included in the same folder, but if you need to share a component or resource between more than one route, you still have to move it into some common folder.
Routes that require a layout are still separated from routes with similar paths but different layouts.
app/├── components/│ ├── authorCard.tsx│ ├── blogPost.tsx│ └── footNote.tsx│├── content/│ ├── goodnight-moon.md│ └── hello-world.md│├── routes/│ ├── _layout.tsx│ ├── _layout._index.tsx│ ├── _layout.about.tsx│ ├── _layout.blog._index/│ │ ├── blogList.tsx│ │ ├── index.tsx│ │ └── paginationButtons.tsx│ ├── _layout.blog._post/│ │ ├── cache.server.ts│ │ └── index.tsx # layout for blog posts│ ├── _layout.blog._post.$slug.tsx # /blog/hello-world│ ├── _layout.blog._post.todo-app.md # /blog/todo-app│ ├── _layout.contact.tsx│ ├── auth.create-account.tsx # /auth/create-account│ ├── auth.login.tsx # /auth/login│ ├── auth.login_.otp/│ │ ├── index.tsx # /auth/login/otp│ │ └── otpInput.tsx│ ├── auth.reset-password.tsx # /auth/reset-password│ ├── blog.$slug.refresh.ts # /blog/hello-world/refresh│ ├── blog.todo-app.example/│ │ ├── db.server.ts│ │ ├── index.tsx # /blog/todo-app/example, no layout│ │ ├── seed.json # not a route, just an asset