Build a Remix App
Use this skill for end-to-end Remix app work. It should help the agent choose
the right layer first, reach for the right package, and avoid the most common
Remix-specific mistakes.
What Remix Is
Remix 3 is a server-first web framework built on Web APIs such as Request,
Response, URL, and FormData. All packages ship from a single npm package,
remix, and are imported via subpath. There is no top-level remix import.
A Remix app has four main pieces:
- Routes in
app/routes.ts define the typed URL contract and power href()
generation.
- Controllers and actions implement that contract and return
Response
objects.
- Middleware composes request lifecycle behavior and populates typed context
via
context.set(Key, value).
- Components render UI with
remix/ui. This is not React. A component
receives a handle, reads current props from handle.props, and returns a
render function.
When To Use This Skill
Use this skill for:
- new features or refactors that touch routing, controllers, middleware, data,
auth, sessions, UI, or tests
- reviewing Remix app code for correctness, architecture, or framework usage
- answering "how should this be structured in Remix?" questions
- finding the right package, reference doc, or default pattern for a task
Load Only The References You Need
Classify the task first, then load the smallest useful reference set. Each
reference file starts with a "What This Covers" section that lists the topics
inside it — read that first to confirm the file is relevant before reading the
rest.
Use the table below to find candidates. Loading more than two or three files at
once is usually a sign that the task hasn't been narrowed enough yet.
| Task involves... |
Start with |
| Defining URLs, writing controllers and actions, returning responses |
references/routing-and-controllers.md |
| Composing the request lifecycle, ordering middleware, bridging to a server |
references/middleware-and-server.md |
| Compiling and serving browser modules, asset URL namespaces, preloads |
references/assets-and-browser-modules.md |
| Parsing input, validating with schemas, defining tables, querying, migrations |
references/data-and-validation.md |
| Per-browser state, login flows, route protection, identity |
references/auth-and-sessions.md |
Component setup, state, lifecycle, updates, queueTask, context |
references/component-model.md |
| Event handlers, styles, refs, click/key behavior, simple animations |
references/mixins-styling-events.md |
clientEntry, run, <Frame>, navigation, <head> |
references/hydration-frames-navigation.md |
| Router tests, component tests, test isolation |
references/testing-patterns.md |
| Spring physics, tweens, layout transitions |
references/animate-elements.md |
| Authoring custom reusable mixins |
references/create-mixins.md |
Common bundles:
- Form or CRUD feature -> routing, data and validation, testing; add auth if
user-specific
- Protected area -> auth and sessions, routing, testing
- Interactive widget -> component model, mixins and styling; add hydration
only if it runs in the browser
- Browser asset pipeline -> assets and browser modules, hydration,
middleware and server
- File upload -> middleware and server, data and validation, testing
- Navigation or frames -> hydration, frames, navigation
Default Workflow
- Classify the change. Decide whether it changes the route contract,
request lifecycle, data model, auth or session behavior, or only UI.
- Start from the server contract. Add or update
app/routes.ts before
wiring handlers or UI.
- Put code in the narrowest owner. Favor route-local code first, then
promote only when reuse is real.
- Make the server path correct before adding browser behavior. A route
should return the right
Response via router.fetch(...) before you add
clientEntry(...), animations, or DOM effects.
- Add middleware deliberately. Keep fast-exit middleware early and
request-enriching middleware later. Export a typed
AppContext from the root
middleware stack and use it in controllers.
- Validate input at the boundary. Parse and validate
Request, FormData,
params, cookies, and external payloads before they reach rendering or
persistence logic.
- Hydrate only when necessary. Prefer server-rendered UI. Use
clientEntry(...) and run(...) only for real browser interactivity or
browser-only APIs.
- Test the narrowest meaningful layer. Prefer router tests for route
behavior. Use component tests when the behavior is truly interactive or
DOM-specific.
- Finish with verification. Re-read the route flow, confirm auth and
authorization boundaries, and run the smallest relevant test and typecheck
loop.
Project Layout
Use these root directories consistently:
app/ for runtime application code
db/ for migrations and local database files
public/ for static assets served as-is
test/ for shared helpers, fixtures, and integration coverage
tmp/ for uploads, caches, local session files, and other scratch data
Inside app/, organize by responsibility:
assets/ for client entrypoints and client-owned browser behavior
controllers/ for route-owned handlers and route-local UI
data/ for schema, queries, persistence setup, migrations, and runtime data
initialization
middleware/ for request lifecycle concerns such as auth, sessions, uploads,
and database injection
ui/ for shared cross-route UI primitives
utils/ only for genuinely cross-layer helpers that do not clearly belong
elsewhere
routes.ts for the route contract
router.ts for router setup and wiring
Placement Precedence
When code could live in multiple places:
- Put it in the narrowest owner first.
- If it belongs to one route, keep it with that route.
- If it is shared UI across route areas, move it to
app/ui/.
- If it is 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 as a last resort for truly cross-layer helpers.
Route Ownership
- Use a flat file in
app/controllers/ for a simple leaf action, such as
app/controllers/home.tsx
- Use a folder with
controller.tsx when a route owns nested routes or multiple
actions, such as app/controllers/account/controller.tsx
- Mirror nested route structure on disk, such as
app/controllers/auth/login/controller.tsx
- Keep route-local UI next to its owner, such as
app/controllers/contact/page.tsx
- Move shared UI to
app/ui/
- If a flat leaf grows child routes or multiple actions, promote it to a
controller folder
Layout Anti-Patterns
- Do not create
app/lib/ as a generic dumping ground
- Do not create
app/components/ as a second shared UI bucket when app/ui/
already owns that role
- Do not put shared cross-route UI in
app/controllers/
- Do not put middleware or persistence helpers in
app/utils/ when they have a
clearer home
- Do not create folders for simple leaf actions unless they are real controllers
Core Remix Rules
- Import from
remix/<subpath>, never import { ... } from 'remix'
- Treat
app/routes.ts as the source of truth for URLs. Use
routes.<name>.href(...) for redirects, links, tests, and internal URL
construction
- Controllers and actions should return explicit
Response objects, including
redirects, 404s, and validation failures. At the route boundary, prefer
returning a Response for expected outcomes (validation errors, conflicts,
not found) over throwing for control flow
- Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules,
and content types are part of the route contract
- Make the server route correct first. A POST should already return the right
HTML, redirect, or error response on its own before
clientEntry(...) layers
interactivity on top
- Validate input at the boundary using
remix/data-schema (and
remix/data-schema/form-data for forms). parseSafe makes the failure path a
return value instead of an exception
- Derive
AppContext from the root middleware stack so get(Database),
get(Session), get(Auth), and similar keys stay typed. If the controller
never reads from context, it doesn't need the harness
- Outside actions and controllers, only use
getContext() when asyncContext()
is in the middleware stack
- Remix Component is not React: read props from
handle.props, keep state in
setup-scope variables, call handle.update() explicitly, and do DOM-sensitive
work in event handlers or queueTask(...), not in render
- Prefer host-element mixins via
mix={mixin(...)} for behavior and styling
instead of inventing custom host prop conventions. Use mix={[...]} only when
composing multiple mixins
- Hydrated
clientEntry(...) props must be serializable. Do not pass functions,
class instances, or opaque runtime objects
Security And Session Defaults
- Never ship demo secrets. In non-test environments, require session and
provider secrets from the environment and fail fast if they are missing
- Use hardened cookies:
httpOnly always, sameSite by default, and secure
when serving over HTTPS
- Regenerate session IDs on login, logout, and privilege changes
- Use
requireAuth() to protect authenticated route areas, but still authorize
resource ownership inside handlers and data writes
- Add CSRF protection when browser forms mutate state using cookie-backed
sessions
- Add CORS only for endpoints that must be called cross-origin. Prefer
same-origin by default
- Prefer JSX or
remix/html-template for HTML generation so escaping stays
correct
- Validate uploads for size, type, and destination. Treat filenames and content
as untrusted input
Testing Defaults
- Prefer server and router tests first. Drive the app with
router.fetch(new Request(...)) and assert on the returned Response
- Build a fresh router per test or per suite so sessions, in-memory storage, and
database state stay isolated
- Use
routes.<name>.href(...) in tests so URLs stay coupled to the route
contract
- For auth or session scenarios, use a test cookie and
createMemorySessionStorage() instead of production storage
- Use component tests only for interactive or DOM-specific behavior. Render with
createRoot(...), interact with the real DOM, and call root.flush() between
steps
- Prefer one representative behavior test over many repetitive assertion
variants
Common Mistakes To Avoid
- Treating Remix Component like React and reaching for hooks or implicit
rerendering
- Importing from a top-level
remix entry instead of a subpath
- Adding
clientEntry(...) before the server-rendered route behavior is correct
- Passing non-serializable props into
clientEntry(...)
- Calling
getContext() without asyncContext() in the middleware stack
- Getting middleware order wrong; fast exits like static files belong early,
request enrichment later
- Skipping boundary validation and trusting raw
FormData, params, cookies, or
external payloads
- Letting route-local domain errors leak out of the controller. Translate
expected outcomes (validation, conflicts, not-found) into the HTTP
Response
the route means to return rather than throwing a custom Error subclass and
catching it elsewhere
- Reaching for
createCookie when a tamper-sensitive or server-managed
per-browser fact really wants remix/session. If editing the value would be a
bug, use a session
- Building a JSON-only RPC layer when a normal form POST, redirect, or resource
route would be simpler. Fetch-from-the-client is a layer on top of sound route
behavior, not a replacement for it
- Treating JSON state endpoints and
<Frame> reloads as mutually exclusive
patterns. Pick the lightest sync mechanism that fits the UX; small widgets may
reasonably poll a JSON endpoint
- Assuming authentication is enough without per-resource authorization checks
- Dropping shared code into vague buckets like
utils.ts, helpers.ts, or
common.ts when ownership is known
- Writing only component tests for a feature whose main behavior is really an
HTTP route concern
Package Map
Use this map to find the right package quickly. Each entry says what the package
is for, not just what it exports. Open the linked reference file when you need
full examples.
Routing, Server, and Responses
remix/fetch-router — the router itself. Use for createRouter, controller
and middleware types, and registering routes
remix/fetch-router/routes — declarative route builders. Use for route,
get, post, put, del, form, resources when defining app/routes.ts
remix/node-fetch-server — adapter from Node's http module to a Fetch-style
router. Use for createRequestListener in server.ts
remix/assets — browser asset server. Use for createAssetServer when
serving compiled scripts and styles, getting public hrefs, and emitting
preloads. Shared compiler options such as target, sourceMaps,
sourceMapSourcePaths, and minify live at the top level
remix/headers — typed header parsers and builders. Use when reading
Accept, Cookie, or setting CacheControl, Vary, etc., instead of
hand-formatting strings
remix/response/redirect — redirect(href, status?). Use for the canonical
"POST then redirect" pattern and other location changes
remix/response/html — createHtmlResponse. Use when you need an HTML
Response from a string or stream without rendering through remix/ui
remix/response/compress — compressResponse. Use when compressing one-off
responses outside the global compression() middleware
remix/response/file — file-download responses. Use for
Content-Disposition: attachment responses
remix/route-pattern — low-level URL matching and generation. Use when
working with raw patterns outside the router (custom matchers, scripts)
remix/fetch-proxy — Fetch-based HTTP proxying. Use to forward a request to
another origin; pass xForwardedHeaders when the upstream needs forwarded
proto, host, and port
Data, Validation, and Persistence
remix/data-schema — schema builders for runtime validation. Use for parse
and parseSafe to validate any input that crosses a trust boundary, and
.transform(...) when validated output should map to a different value or
type
remix/data-schema/checks — common check helpers (email, minLength,
maxLength, etc.). Use to compose into a schema
remix/data-schema/coerce — coercion helpers for strings, numbers, booleans,
dates, and ids. Use when input arrives as a string but should be a typed value
remix/data-schema/form-data — f.object and f.field for parsing
FormData directly. Use in actions that read browser forms
remix/data-table — typed tables and a Database interface. Use for table,
column, createDatabase when modeling persisted data
remix/data-table-sqlite, remix/data-table-postgres,
remix/data-table-mysql — adapters. Use to back createDatabase with a real
engine. SQLite accepts Node, Bun, and compatible synchronous clients with the
shared prepare/exec surface
remix/data-table/migrations — migration authoring and runners. Use for
createMigration, createMigrationRunner
remix/data-table/migrations/node — loadMigrations from disk. Use in
startup scripts that apply migrations
remix/data-table/operators — query operators such as inList(...). Use when
where clauses need set or comparison logic
Auth, Sessions, and Cookies
remix/session — the Session object: get, set, flash, unset,
regenerateId. Use for any per-browser state where tampering would be a bug
(login, "I submitted this form already", cart, flash messages)
remix/session-middleware — session(cookie, storage). Use to wire a session
cookie and storage backend into the root middleware stack
remix/session/fs-storage, remix/session/memory-storage,
remix/session/cookie-storage — storage backends. Use fs-storage for
single-process apps, memory-storage for tests, cookie-storage for
stateless deployments where data fits in a cookie
remix/session-storage-redis — Redis-backed storage. Use for multi-process or
multi-host deployments
remix/session-storage-memcache — Memcache-backed storage. Same multi-host
use case as Redis
remix/cookie — createCookie for plain signed/unsigned cookies. Use for
non-sensitive preferences where the client is allowed to control the value
(theme, locale, dismissed banner). For state where tampering matters, prefer
remix/session
remix/auth — credentials, OAuth, OIDC, and Atmosphere providers. Use to
define how identity is verified, start/finish external login, and refresh
stored OAuth/OIDC token bundles with refreshExternalAuth(...)
remix/auth-middleware — auth({ schemes }), requireAuth, the Auth
context key. Use to resolve identity into the request context and to gate
routes
UI, Hydration, and Browser Behavior
remix/ui — the component runtime: components, core mixins, clientEntry,
run, <Frame>, navigation helpers, and createRoot. Use for app UI
behavior
remix/ui/server — server rendering: renderToStream, renderToString. Use
in the render(...) helper that returns HTML responses
remix/ui/animation — animation APIs: animateEntrance, animateExit,
animateLayout, spring, tween, and easings
remix/ui/<primitive> — UI primitives, mixins, glyphs, and theme helpers.
Import from remix/ui/accordion, remix/ui/button, remix/ui/select, etc.
remix/ui/test — component test rendering helpers such as render
remix/ui/jsx-runtime — JSX transform target. Configured in tsconfig.json,
rarely imported directly
remix/html-template — escaped HTML template literals. Use when generating
HTML outside the component system (RSS feeds, email bodies, error pages)
remix/file-storage — backend-agnostic File storage interface. Use as the
type bound for upload destinations
remix/file-storage/fs, remix/file-storage/memory, remix/file-storage-s3
— storage backends. Use to implement an upload destination
Middleware
remix/static-middleware — staticFiles(dir). Use to serve files from
public/ exactly as they exist on disk
remix/form-data-middleware — formData(). Use to parse FormData once and
expose it via get(FormData) instead of calling await request.formData() in
each action
remix/form-data-parser — lower-level parseFormData, FileUpload. Use when
implementing custom upload handlers. Upload handler errors propagate directly
remix/multipart-parser and remix/multipart-parser/node — low-level
multipart stream parsing. MultipartPart.headers is a plain object keyed by
lower-case header name; read values with bracket notation such as
part.headers['content-type']
remix/compression-middleware — compression(). Use globally for text-like
responses
remix/logger-middleware — logger(). Use in development for request logs;
pass colors to force terminal color output on or off
remix/method-override-middleware — methodOverride(). Use when HTML forms
need PUT, PATCH, or DELETE
remix/async-context-middleware — asyncContext(), getContext(). Use when
helpers outside actions need request context without threading it through
every call
remix/cors-middleware — cors(opts?). Use for endpoints called cross-origin
remix/csrf-middleware — csrf(opts?). Use when session-backed forms mutate
state and need synchronizer-token CSRF protection
remix/cop-middleware — cross-origin protection. Use to reject unsafe
cross-origin browser requests
Test
remix/test — describe, it, and lifecycle hooks. Use as the test
framework
remix/test/cli — programmatic test runner APIs such as runRemixTest
remix/cli — programmatic Remix CLI API. Use the remix executable for
project commands such as remix test, remix routes, and remix doctor
remix/assert — assertion helpers. Use in place of node:assert so messages
render cleanly in the runner
remix/terminal — ANSI styles, color detection, style factories, and testable
terminal streams. Use for CLIs and terminal output instead of hand-rolled
escape sequences
Canonical Patterns
Define routes first
import { form, get, post, resources, route } from 'remix/fetch-router/routes'
export const routes = route({
home: '/',
contact: form('contact'),
books: {
index: '/books',
show: '/books/:slug',
},
auth: route('auth', {
login: form('login'),
logout: post('logout'),
}),
admin: route('admin', {
index: get('/'),
books: resources('books', { param: 'bookId' }),
}),
})
Type controllers against the route contract
import type { Controller } from 'remix/fetch-router'
import type { AppContext } from '../router.ts'
import { routes } from '../routes.ts'
export default {
actions: {
async index({ get }) {
let db = get(Database)
let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<BooksIndexPage allBooks={allBooks} />)
},
async show({ get, params }) {
let db = get(Database)
let book = await db.findOne(books, { where: { slug: params.slug } })
if (!book) return new Response('Not Found', { status: 404 })
return render(<BookShowPage book={book} />)
},
},
} satisfies Controller<typeof routes.books, AppContext>
Compose middleware deliberately
import {
createRouter,
type AnyParams,
type MiddlewareContext,
type WithParams,
} from 'remix/fetch-router'
export type RootMiddleware = [
ReturnType<typeof formData>,
ReturnType<typeof session>,
ReturnType<typeof loadDatabase>,
ReturnType<typeof loadAuth>,
]
export type AppContext<params extends AnyParams = AnyParams> = WithParams<
MiddlewareContext<RootMiddleware>,
params
>
let middleware = []
if (process.env.NODE_ENV === 'development') {
middleware.push(logger())
}
middleware.push(compression())
middleware.push(staticFiles('./public'))
middleware.push(formData())
middleware.push(methodOverride())
middleware.push(session(cookie, storage))
middleware.push(asyncContext())
middleware.push(loadDatabase())
middleware.push(loadAuth())
let router = createRouter({ middleware })
Mutate, validate, and respond
import { redirect } from 'remix/response/redirect'
import * as s from 'remix/data-schema'
import * as f from 'remix/data-schema/form-data'
import { Session } from 'remix/session'
import { Database } from 'remix/data-table'
let bookSchema = f.object({
slug: f.field(s.string()),
title: f.field(s.string()),
})
export default {
actions: {
async create({ get }) {
let parsed = s.parseSafe(bookSchema, get(FormData))
if (!parsed.success) {
return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
}
let db = get(Database)
let book = await db.create(books, parsed.value)
let session = get(Session)
session.flash('message', `Added ${book.title}.`)
return redirect(routes.books.show.href({ slug: book.slug }))
},
},
} satisfies Controller<typeof routes.books, AppContext>
This shape works without JavaScript, returns a Response for every outcome, and
is ready for clientEntry(...) interactivity when the UI needs it.
Build UI from handle props plus render
import { on, type Handle } from 'remix/ui'
function Counter(handle: Handle<{ initialCount?: number; label: string }>) {
let count = handle.props.initialCount ?? 0
return () => (
<button
mix={on('click', () => {
count++
handle.update()
})}
>
{handle.props.label}: {count}
</button>
)
}
Only add clientEntry(...) and run(...) when the component needs browser
interactivity or browser-only APIs.
Source: kentcdodds/kody — distributed by TomeVault.
1---2name: remix3description: Use this skill for end-to-end Remix app work. It should help the agent choose4---56# Build a Remix App78Use this skill for end-to-end Remix app work. It should help the agent choose9the right layer first, reach for the right package, and avoid the most common10Remix-specific mistakes.1112## What Remix Is1314Remix 3 is a server-first web framework built on Web APIs such as `Request`,15`Response`, `URL`, and `FormData`. All packages ship from a single npm package,16`remix`, and are imported via subpath. There is no top-level `remix` import.1718A Remix app has four main pieces:1920- **Routes** in `app/routes.ts` define the typed URL contract and power `href()`21 generation.22- **Controllers and actions** implement that contract and return `Response`23 objects.24- **Middleware** composes request lifecycle behavior and populates typed context25 via `context.set(Key, value)`.26- **Components** render UI with `remix/ui`. This is not React. A component27 receives a `handle`, reads current props from `handle.props`, and returns a28 render function.2930## When To Use This Skill3132Use this skill for:3334- new features or refactors that touch routing, controllers, middleware, data,35 auth, sessions, UI, or tests36- reviewing Remix app code for correctness, architecture, or framework usage37- answering "how should this be structured in Remix?" questions38- finding the right package, reference doc, or default pattern for a task3940## Load Only The References You Need4142Classify the task first, then load the smallest useful reference set. Each43reference file starts with a "What This Covers" section that lists the topics44inside it — read that first to confirm the file is relevant before reading the45rest.4647Use the table below to find candidates. Loading more than two or three files at48once is usually a sign that the task hasn't been narrowed enough yet.4950| Task involves... | Start with |51| ----------------------------------------------------------------------------- | ------------------------------------------- |52| Defining URLs, writing controllers and actions, returning responses | `references/routing-and-controllers.md` |53| Composing the request lifecycle, ordering middleware, bridging to a server | `references/middleware-and-server.md` |54| Compiling and serving browser modules, asset URL namespaces, preloads | `references/assets-and-browser-modules.md` |55| Parsing input, validating with schemas, defining tables, querying, migrations | `references/data-and-validation.md` |56| Per-browser state, login flows, route protection, identity | `references/auth-and-sessions.md` |57| Component setup, state, lifecycle, updates, `queueTask`, context | `references/component-model.md` |58| Event handlers, styles, refs, click/key behavior, simple animations | `references/mixins-styling-events.md` |59| `clientEntry`, `run`, `<Frame>`, navigation, `<head>` | `references/hydration-frames-navigation.md` |60| Router tests, component tests, test isolation | `references/testing-patterns.md` |61| Spring physics, tweens, layout transitions | `references/animate-elements.md` |62| Authoring custom reusable mixins | `references/create-mixins.md` |6364Common bundles:6566- **Form or CRUD feature** -> routing, data and validation, testing; add auth if67 user-specific68- **Protected area** -> auth and sessions, routing, testing69- **Interactive widget** -> component model, mixins and styling; add hydration70 only if it runs in the browser71- **Browser asset pipeline** -> assets and browser modules, hydration,72 middleware and server73- **File upload** -> middleware and server, data and validation, testing74- **Navigation or frames** -> hydration, frames, navigation7576## Default Workflow77781. **Classify the change.** Decide whether it changes the route contract,79 request lifecycle, data model, auth or session behavior, or only UI.802. **Start from the server contract.** Add or update `app/routes.ts` before81 wiring handlers or UI.823. **Put code in the narrowest owner.** Favor route-local code first, then83 promote only when reuse is real.844. **Make the server path correct before adding browser behavior.** A route85 should return the right `Response` via `router.fetch(...)` before you add86 `clientEntry(...)`, animations, or DOM effects.875. **Add middleware deliberately.** Keep fast-exit middleware early and88 request-enriching middleware later. Export a typed `AppContext` from the root89 middleware stack and use it in controllers.906. **Validate input at the boundary.** Parse and validate `Request`, `FormData`,91 params, cookies, and external payloads before they reach rendering or92 persistence logic.937. **Hydrate only when necessary.** Prefer server-rendered UI. Use94 `clientEntry(...)` and `run(...)` only for real browser interactivity or95 browser-only APIs.968. **Test the narrowest meaningful layer.** Prefer router tests for route97 behavior. Use component tests when the behavior is truly interactive or98 DOM-specific.999. **Finish with verification.** Re-read the route flow, confirm auth and100 authorization boundaries, and run the smallest relevant test and typecheck101 loop.102103## Project Layout104105Use these root directories consistently:106107- `app/` for runtime application code108- `db/` for migrations and local database files109- `public/` for static assets served as-is110- `test/` for shared helpers, fixtures, and integration coverage111- `tmp/` for uploads, caches, local session files, and other scratch data112113Inside `app/`, organize by responsibility:114115- `assets/` for client entrypoints and client-owned browser behavior116- `controllers/` for route-owned handlers and route-local UI117- `data/` for schema, queries, persistence setup, migrations, and runtime data118 initialization119- `middleware/` for request lifecycle concerns such as auth, sessions, uploads,120 and database injection121- `ui/` for shared cross-route UI primitives122- `utils/` only for genuinely cross-layer helpers that do not clearly belong123 elsewhere124- `routes.ts` for the route contract125- `router.ts` for router setup and wiring126127### Placement Precedence128129When code could live in multiple places:1301311. Put it in the narrowest owner first.1322. If it belongs to one route, keep it with that route.1333. If it is shared UI across route areas, move it to `app/ui/`.1344. If it is request lifecycle setup, keep it in `app/middleware/`.1355. If it is schema, query, persistence, or startup data logic, keep it in136 `app/data/`.1376. Use `app/utils/` only as a last resort for truly cross-layer helpers.138139### Route Ownership140141- Use a flat file in `app/controllers/` for a simple leaf action, such as142 `app/controllers/home.tsx`143- Use a folder with `controller.tsx` when a route owns nested routes or multiple144 actions, such as `app/controllers/account/controller.tsx`145- Mirror nested route structure on disk, such as146 `app/controllers/auth/login/controller.tsx`147- Keep route-local UI next to its owner, such as148 `app/controllers/contact/page.tsx`149- Move shared UI to `app/ui/`150- If a flat leaf grows child routes or multiple actions, promote it to a151 controller folder152153### Layout Anti-Patterns154155- Do not create `app/lib/` as a generic dumping ground156- Do not create `app/components/` as a second shared UI bucket when `app/ui/`157 already owns that role158- Do not put shared cross-route UI in `app/controllers/`159- Do not put middleware or persistence helpers in `app/utils/` when they have a160 clearer home161- Do not create folders for simple leaf actions unless they are real controllers162163## Core Remix Rules164165- Import from `remix/<subpath>`, never `import { ... } from 'remix'`166- Treat `app/routes.ts` as the source of truth for URLs. Use167 `routes.<name>.href(...)` for redirects, links, tests, and internal URL168 construction169- Controllers and actions should return explicit `Response` objects, including170 redirects, 404s, and validation failures. At the route boundary, prefer171 returning a `Response` for expected outcomes (validation errors, conflicts,172 not found) over throwing for control flow173- Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules,174 and content types are part of the route contract175- Make the server route correct first. A POST should already return the right176 HTML, redirect, or error response on its own before `clientEntry(...)` layers177 interactivity on top178- Validate input at the boundary using `remix/data-schema` (and179 `remix/data-schema/form-data` for forms). `parseSafe` makes the failure path a180 return value instead of an exception181- Derive `AppContext` from the root middleware stack so `get(Database)`,182 `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller183 never reads from context, it doesn't need the harness184- Outside actions and controllers, only use `getContext()` when `asyncContext()`185 is in the middleware stack186- Remix Component is not React: read props from `handle.props`, keep state in187 setup-scope variables, call `handle.update()` explicitly, and do DOM-sensitive188 work in event handlers or `queueTask(...)`, not in render189- Prefer host-element mixins via `mix={mixin(...)}` for behavior and styling190 instead of inventing custom host prop conventions. Use `mix={[...]}` only when191 composing multiple mixins192- Hydrated `clientEntry(...)` props must be serializable. Do not pass functions,193 class instances, or opaque runtime objects194195## Security And Session Defaults196197- Never ship demo secrets. In non-test environments, require session and198 provider secrets from the environment and fail fast if they are missing199- Use hardened cookies: `httpOnly` always, `sameSite` by default, and `secure`200 when serving over HTTPS201- Regenerate session IDs on login, logout, and privilege changes202- Use `requireAuth()` to protect authenticated route areas, but still authorize203 resource ownership inside handlers and data writes204- Add CSRF protection when browser forms mutate state using cookie-backed205 sessions206- Add CORS only for endpoints that must be called cross-origin. Prefer207 same-origin by default208- Prefer JSX or `remix/html-template` for HTML generation so escaping stays209 correct210- Validate uploads for size, type, and destination. Treat filenames and content211 as untrusted input212213## Testing Defaults214215- Prefer server and router tests first. Drive the app with216 `router.fetch(new Request(...))` and assert on the returned `Response`217- Build a fresh router per test or per suite so sessions, in-memory storage, and218 database state stay isolated219- Use `routes.<name>.href(...)` in tests so URLs stay coupled to the route220 contract221- For auth or session scenarios, use a test cookie and222 `createMemorySessionStorage()` instead of production storage223- Use component tests only for interactive or DOM-specific behavior. Render with224 `createRoot(...)`, interact with the real DOM, and call `root.flush()` between225 steps226- Prefer one representative behavior test over many repetitive assertion227 variants228229## Common Mistakes To Avoid230231- Treating Remix Component like React and reaching for hooks or implicit232 rerendering233- Importing from a top-level `remix` entry instead of a subpath234- Adding `clientEntry(...)` before the server-rendered route behavior is correct235- Passing non-serializable props into `clientEntry(...)`236- Calling `getContext()` without `asyncContext()` in the middleware stack237- Getting middleware order wrong; fast exits like static files belong early,238 request enrichment later239- Skipping boundary validation and trusting raw `FormData`, params, cookies, or240 external payloads241- Letting route-local domain errors leak out of the controller. Translate242 expected outcomes (validation, conflicts, not-found) into the HTTP `Response`243 the route means to return rather than throwing a custom `Error` subclass and244 catching it elsewhere245- Reaching for `createCookie` when a tamper-sensitive or server-managed246 per-browser fact really wants `remix/session`. If editing the value would be a247 bug, use a session248- Building a JSON-only RPC layer when a normal form POST, redirect, or resource249 route would be simpler. Fetch-from-the-client is a layer on top of sound route250 behavior, not a replacement for it251- Treating JSON state endpoints and `<Frame>` reloads as mutually exclusive252 patterns. Pick the lightest sync mechanism that fits the UX; small widgets may253 reasonably poll a JSON endpoint254- Assuming authentication is enough without per-resource authorization checks255- Dropping shared code into vague buckets like `utils.ts`, `helpers.ts`, or256 `common.ts` when ownership is known257- Writing only component tests for a feature whose main behavior is really an258 HTTP route concern259260## Package Map261262Use this map to find the right package quickly. Each entry says what the package263is for, not just what it exports. Open the linked reference file when you need264full examples.265266### Routing, Server, and Responses267268- `remix/fetch-router` — the router itself. Use for `createRouter`, controller269 and middleware types, and registering routes270- `remix/fetch-router/routes` — declarative route builders. Use for `route`,271 `get`, `post`, `put`, `del`, `form`, `resources` when defining `app/routes.ts`272- `remix/node-fetch-server` — adapter from Node's `http` module to a Fetch-style273 router. Use for `createRequestListener` in `server.ts`274- `remix/assets` — browser asset server. Use for `createAssetServer` when275 serving compiled scripts and styles, getting public hrefs, and emitting276 preloads. Shared compiler options such as `target`, `sourceMaps`,277 `sourceMapSourcePaths`, and `minify` live at the top level278- `remix/headers` — typed header parsers and builders. Use when reading279 `Accept`, `Cookie`, or setting `CacheControl`, `Vary`, etc., instead of280 hand-formatting strings281- `remix/response/redirect` — `redirect(href, status?)`. Use for the canonical282 "POST then redirect" pattern and other location changes283- `remix/response/html` — `createHtmlResponse`. Use when you need an HTML284 `Response` from a string or stream without rendering through `remix/ui`285- `remix/response/compress` — `compressResponse`. Use when compressing one-off286 responses outside the global `compression()` middleware287- `remix/response/file` — file-download responses. Use for288 `Content-Disposition: attachment` responses289- `remix/route-pattern` — low-level URL matching and generation. Use when290 working with raw patterns outside the router (custom matchers, scripts)291- `remix/fetch-proxy` — Fetch-based HTTP proxying. Use to forward a request to292 another origin; pass `xForwardedHeaders` when the upstream needs forwarded293 proto, host, and port294295### Data, Validation, and Persistence296297- `remix/data-schema` — schema builders for runtime validation. Use for `parse`298 and `parseSafe` to validate any input that crosses a trust boundary, and299 `.transform(...)` when validated output should map to a different value or300 type301- `remix/data-schema/checks` — common check helpers (`email`, `minLength`,302 `maxLength`, etc.). Use to compose into a schema303- `remix/data-schema/coerce` — coercion helpers for strings, numbers, booleans,304 dates, and ids. Use when input arrives as a string but should be a typed value305- `remix/data-schema/form-data` — `f.object` and `f.field` for parsing306 `FormData` directly. Use in actions that read browser forms307- `remix/data-table` — typed tables and a `Database` interface. Use for `table`,308 `column`, `createDatabase` when modeling persisted data309- `remix/data-table-sqlite`, `remix/data-table-postgres`,310 `remix/data-table-mysql` — adapters. Use to back `createDatabase` with a real311 engine. SQLite accepts Node, Bun, and compatible synchronous clients with the312 shared `prepare`/`exec` surface313- `remix/data-table/migrations` — migration authoring and runners. Use for314 `createMigration`, `createMigrationRunner`315- `remix/data-table/migrations/node` — `loadMigrations` from disk. Use in316 startup scripts that apply migrations317- `remix/data-table/operators` — query operators such as `inList(...)`. Use when318 `where` clauses need set or comparison logic319320### Auth, Sessions, and Cookies321322- `remix/session` — the `Session` object: `get`, `set`, `flash`, `unset`,323 `regenerateId`. Use for any per-browser state where tampering would be a bug324 (login, "I submitted this form already", cart, flash messages)325- `remix/session-middleware` — `session(cookie, storage)`. Use to wire a session326 cookie and storage backend into the root middleware stack327- `remix/session/fs-storage`, `remix/session/memory-storage`,328 `remix/session/cookie-storage` — storage backends. Use `fs-storage` for329 single-process apps, `memory-storage` for tests, `cookie-storage` for330 stateless deployments where data fits in a cookie331- `remix/session-storage-redis` — Redis-backed storage. Use for multi-process or332 multi-host deployments333- `remix/session-storage-memcache` — Memcache-backed storage. Same multi-host334 use case as Redis335- `remix/cookie` — `createCookie` for plain signed/unsigned cookies. Use for336 non-sensitive preferences where the client is allowed to control the value337 (theme, locale, dismissed banner). For state where tampering matters, prefer338 `remix/session`339- `remix/auth` — credentials, OAuth, OIDC, and Atmosphere providers. Use to340 define how identity is verified, start/finish external login, and refresh341 stored OAuth/OIDC token bundles with `refreshExternalAuth(...)`342- `remix/auth-middleware` — `auth({ schemes })`, `requireAuth`, the `Auth`343 context key. Use to resolve identity into the request context and to gate344 routes345346### UI, Hydration, and Browser Behavior347348- `remix/ui` — the component runtime: components, core mixins, `clientEntry`,349 `run`, `<Frame>`, navigation helpers, and `createRoot`. Use for app UI350 behavior351- `remix/ui/server` — server rendering: `renderToStream`, `renderToString`. Use352 in the `render(...)` helper that returns HTML responses353- `remix/ui/animation` — animation APIs: `animateEntrance`, `animateExit`,354 `animateLayout`, `spring`, `tween`, and `easings`355- `remix/ui/<primitive>` — UI primitives, mixins, glyphs, and theme helpers.356 Import from `remix/ui/accordion`, `remix/ui/button`, `remix/ui/select`, etc.357- `remix/ui/test` — component test rendering helpers such as `render`358- `remix/ui/jsx-runtime` — JSX transform target. Configured in `tsconfig.json`,359 rarely imported directly360- `remix/html-template` — escaped HTML template literals. Use when generating361 HTML outside the component system (RSS feeds, email bodies, error pages)362- `remix/file-storage` — backend-agnostic `File` storage interface. Use as the363 type bound for upload destinations364- `remix/file-storage/fs`, `remix/file-storage/memory`, `remix/file-storage-s3`365 — storage backends. Use to implement an upload destination366367### Middleware368369- `remix/static-middleware` — `staticFiles(dir)`. Use to serve files from370 `public/` exactly as they exist on disk371- `remix/form-data-middleware` — `formData()`. Use to parse `FormData` once and372 expose it via `get(FormData)` instead of calling `await request.formData()` in373 each action374- `remix/form-data-parser` — lower-level `parseFormData`, `FileUpload`. Use when375 implementing custom upload handlers. Upload handler errors propagate directly376- `remix/multipart-parser` and `remix/multipart-parser/node` — low-level377 multipart stream parsing. `MultipartPart.headers` is a plain object keyed by378 lower-case header name; read values with bracket notation such as379 `part.headers['content-type']`380- `remix/compression-middleware` — `compression()`. Use globally for text-like381 responses382- `remix/logger-middleware` — `logger()`. Use in development for request logs;383 pass `colors` to force terminal color output on or off384- `remix/method-override-middleware` — `methodOverride()`. Use when HTML forms385 need `PUT`, `PATCH`, or `DELETE`386- `remix/async-context-middleware` — `asyncContext()`, `getContext()`. Use when387 helpers outside actions need request context without threading it through388 every call389- `remix/cors-middleware` — `cors(opts?)`. Use for endpoints called cross-origin390- `remix/csrf-middleware` — `csrf(opts?)`. Use when session-backed forms mutate391 state and need synchronizer-token CSRF protection392- `remix/cop-middleware` — cross-origin protection. Use to reject unsafe393 cross-origin browser requests394395### Test396397- `remix/test` — `describe`, `it`, and lifecycle hooks. Use as the test398 framework399- `remix/test/cli` — programmatic test runner APIs such as `runRemixTest`400- `remix/cli` — programmatic Remix CLI API. Use the `remix` executable for401 project commands such as `remix test`, `remix routes`, and `remix doctor`402- `remix/assert` — assertion helpers. Use in place of `node:assert` so messages403 render cleanly in the runner404- `remix/terminal` — ANSI styles, color detection, style factories, and testable405 terminal streams. Use for CLIs and terminal output instead of hand-rolled406 escape sequences407408## Canonical Patterns409410### Define routes first411412```typescript413import { form, get, post, resources, route } from 'remix/fetch-router/routes'414415export const routes = route({416 home: '/',417 contact: form('contact'),418 books: {419 index: '/books',420 show: '/books/:slug',421 },422 auth: route('auth', {423 login: form('login'),424 logout: post('logout'),425 }),426 admin: route('admin', {427 index: get('/'),428 books: resources('books', { param: 'bookId' }),429 }),430})431```432433### Type controllers against the route contract434435```typescript436import type { Controller } from 'remix/fetch-router'437438import type { AppContext } from '../router.ts'439import { routes } from '../routes.ts'440441export default {442 actions: {443 async index({ get }) {444 let db = get(Database)445 let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })446 return render(<BooksIndexPage allBooks={allBooks} />)447 },448 async show({ get, params }) {449 let db = get(Database)450 let book = await db.findOne(books, { where: { slug: params.slug } })451 if (!book) return new Response('Not Found', { status: 404 })452 return render(<BookShowPage book={book} />)453 },454 },455} satisfies Controller<typeof routes.books, AppContext>456```457458### Compose middleware deliberately459460```typescript461import {462 createRouter,463 type AnyParams,464 type MiddlewareContext,465 type WithParams,466} from 'remix/fetch-router'467468export type RootMiddleware = [469 ReturnType<typeof formData>,470 ReturnType<typeof session>,471 ReturnType<typeof loadDatabase>,472 ReturnType<typeof loadAuth>,473]474475export type AppContext<params extends AnyParams = AnyParams> = WithParams<476 MiddlewareContext<RootMiddleware>,477 params478>479480let middleware = []481482if (process.env.NODE_ENV === 'development') {483 middleware.push(logger())484}485486middleware.push(compression())487middleware.push(staticFiles('./public'))488middleware.push(formData())489middleware.push(methodOverride())490middleware.push(session(cookie, storage))491middleware.push(asyncContext())492middleware.push(loadDatabase())493middleware.push(loadAuth())494495let router = createRouter({ middleware })496```497498### Mutate, validate, and respond499500```typescript501import { redirect } from 'remix/response/redirect'502import * as s from 'remix/data-schema'503import * as f from 'remix/data-schema/form-data'504import { Session } from 'remix/session'505import { Database } from 'remix/data-table'506507let bookSchema = f.object({508 slug: f.field(s.string()),509 title: f.field(s.string()),510})511512export default {513 actions: {514 async create({ get }) {515 let parsed = s.parseSafe(bookSchema, get(FormData))516 if (!parsed.success) {517 return render(<NewBookPage errors={parsed.issues} />, { status: 400 })518 }519520 let db = get(Database)521 let book = await db.create(books, parsed.value)522523 let session = get(Session)524 session.flash('message', `Added ${book.title}.`)525526 return redirect(routes.books.show.href({ slug: book.slug }))527 },528 },529} satisfies Controller<typeof routes.books, AppContext>530```531532This shape works without JavaScript, returns a `Response` for every outcome, and533is ready for `clientEntry(...)` interactivity when the UI needs it.534535### Build UI from handle props plus render536537```tsx538import { on, type Handle } from 'remix/ui'539540function Counter(handle: Handle<{ initialCount?: number; label: string }>) {541 let count = handle.props.initialCount ?? 0542543 return () => (544 <button545 mix={on('click', () => {546 count++547 handle.update()548 })}549 >550 {handle.props.label}: {count}551 </button>552 )553}554```555556Only add `clientEntry(...)` and `run(...)` when the component needs browser557interactivity or browser-only APIs.558559---560> Source: [kentcdodds/kody](https://github.com/kentcdodds/kody) — distributed by [TomeVault](https://tomevault.io).561<!-- tomevault:4.0:skill_md:2026-07-04 -->