remix-project-layout
Remix Project Layout
Use this skill when defining, reviewing, or bootstrapping the on-disk layout of a Remix application.
This skill is about structure and conventions. It defines where code belongs, how route ownership maps to files on disk, and how a Remix app should be organized as it grows. When the user wants a new app scaffolded, run the bundled script instead of recreating the starter files by hand.
Root Layout
Use these root directories consistently:
app/for runtime application codedb/for database artifacts such as migrations and SQLite filespublic/for static files served as-istest/for shared test helpers, fixtures, and cross-app integration coveragetmp/for runtime scratch files such as uploads, caches, or local session files
App Layout
Inside app/, organize code by responsibility:
assets/for client entrypoints and client-owned behaviorcontrollers/for route-owned handlers and route-local UIdata/for schema, queries, persistence setup, and runtime data initializationmiddleware/for request lifecycle concerns such as auth, sessions, database injection, and uploadsui/for shared cross-route UI primitivesutils/for genuinely cross-layer runtime helpersroutes.tsfor the route contractrouter.tsfor router setup and route wiring
Placement Precedence
When code could plausibly live in more than one place, use this order of precedence:
- Put code in the narrowest owner first.
- If it belongs to one route, keep it with that route.
- If it is reused across route areas but is still UI, move it to
app/ui/. - If it is part of request lifecycle setup, keep it in
app/middleware/. - If it is schema, query, persistence, or startup data logic, keep it in
app/data/. - Use
app/utils/only when the code is genuinely cross-layer and does not clearly belong to one of the other app layers.
Prefer moving code to a narrower owner over introducing generic shared buckets.
Route Ownership
The disk layout should make it possible to start from a route key in app/routes.ts and find the
implementation immediately.
Flat Leaf Routes
Use a flat file in app/controllers/ when a route is implemented by one exported BuildAction.
Examples:
routes.home->app/controllers/home.tsxroutes.about->app/controllers/about.tsxroutes.search->app/controllers/search.tsxroutes.uploads->app/controllers/uploads.tsx
Flat leaf route files are self-contained. If a helper component is used only by that route, keep it in the same module.
Controller Folders
Use a folder with controller.tsx when the route is implemented by a Controller, owns nested
child routes, or owns multiple actions such as index, action, show, or update.
Examples:
routes.contact->app/controllers/contact/controller.tsxroutes.auth->app/controllers/auth/controller.tsxroutes.account->app/controllers/account/controller.tsxroutes.cart->app/controllers/cart/controller.tsx
Nested Route Objects
If a route is nested in app/routes.ts, mirror that nesting on disk.
Examples:
routes.auth.login->app/controllers/auth/login/controller.tsxroutes.account.settings->app/controllers/account/settings/controller.tsxroutes.account.orders->app/controllers/account/orders/controller.tsxroutes.cart.api->app/controllers/cart/api/controller.tsxroutes.admin.users->app/controllers/admin/users/controller.tsx
Shared UI
If UI is reused across route areas, it belongs in app/ui/, not under app/controllers/.
Examples:
app/ui/document.tsxapp/ui/layout.tsxapp/ui/form-field.tsxapp/ui/restful-form.tsx
Only keep a component inside a controller folder when that UI is owned by that route or controller feature.
Route-Local UI
If a page component or helper is used only by one action or controller feature, keep it next to the owning route.
Examples:
app/controllers/contact/page.tsxapp/controllers/auth/login/page.tsxapp/controllers/admin/books/form.tsxapp/controllers/books/index-page.tsx
For flat leaf actions, route-local UI lives in the same file as the action.
Promotion Rule
If a route starts as a flat leaf file and later grows child routes or multiple actions, promote it from:
app/controllers/home.tsx
to:
app/controllers/home/controller.tsx
Do this only when the route actually needs it. Do not create controller folders preemptively for every leaf route.
Naming Conventions
Use predictable file names so route ownership is obvious without opening files:
controller.tsxfor controller entrypointspage.tsxfor a single controller-owned page moduleindex-page.tsx,show-page.tsx,edit-page.tsx, andform.tsxfor resource-style controller UI- flat action files named after the route key, such as
home.tsx,about.tsx,search.tsx, oruploads.tsx - colocated tests named after the route owner, such as
home.test.tsorcontroller.test.ts
Do not invent one-off naming schemes when an existing convention already fits.
Bootstrap
When the user wants this layout scaffolded into a new directory, run:
node skills/remix-project-layout/scripts/bootstrap_remix_application.ts <target-dir>
Optional flags:
--app-name <name>to override the generated app name--remix-version <version>to override the defaultremixversion--forceto write into a non-empty target directory
The script generates the starter app, including README.md, route handlers, shared UI, test
helpers, and the root directory structure described in this skill.
Anti-Patterns
- Do not create
app/lib/as a generic dumping ground. - Do not create
app/components/as a second shared UI bucket whenapp/ui/already owns that role. - Do not put shared cross-route UI in
app/controllers/. - Do not put route-owned page modules in
app/ui/. - Do not put middleware, session, auth, or database lifecycle helpers in
app/utils/when they belong inapp/middleware/. - Do not put schema, query, or database setup code in
app/utils/when it belongs inapp/data/. - Do not create folders for simple leaf actions unless they are real controllers.
- Do not split solo actions across multiple files.
- Do not create vague files like
helpers.ts,common.ts, ormisc.tsunless the name is truly accurate for the module's ownership.