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
,
,
,
and
. All packages ship from a single npm package,
, and are imported via
subpath. There is no top-level
import.
A Remix app has four main pieces:
- Routes in define the typed URL contract and power generation.
- Controllers and actions implement that contract and return objects.
- Middleware composes request lifecycle behavior and populates typed context via
.
- Components render UI with . This is not React. A component receives a
, reads current props from , 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, , context | references/component-model.md
|
| Event handlers, styles, refs, click/key behavior, simple animations | references/mixins-styling-events.md
|
| , , , navigation, | 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 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
via before you add , animations, or DOM
effects.
- Add middleware deliberately. Keep fast-exit middleware early and request-enriching
middleware later. Export a typed from the root middleware stack and use it in
controllers.
- Validate input at the boundary. Parse and validate , , params, cookies,
and external payloads before they reach rendering or persistence logic.
- Hydrate only when necessary. Prefer server-rendered UI. Use and
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:
- for runtime application code
- for migrations and local database files
- for static assets served as-is
- for shared helpers, fixtures, and integration coverage
- for uploads, caches, local session files, and other scratch data
Inside
, organize by responsibility:
- for client entrypoints and client-owned browser behavior
- for route-owned handlers and route-local UI
- for schema, queries, persistence setup, migrations, and runtime data initialization
- for request lifecycle concerns such as auth, sessions, uploads, and database
injection
- for shared cross-route UI primitives
- only for genuinely cross-layer helpers that do not clearly belong elsewhere
- for the route contract
- 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 .
- If it is request lifecycle setup, keep it in .
- If it is schema, query, persistence, or startup data logic, keep it in .
- Use only as a last resort for truly cross-layer helpers.
Route Ownership
- Use a flat file in for a simple leaf action, such as
- Use a folder with 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
- If a flat leaf grows child routes or multiple actions, promote it to a controller folder
Layout Anti-Patterns
- Do not create as a generic dumping ground
- Do not create as a second shared UI bucket when already owns that
role
- Do not put shared cross-route UI in
- Do not put middleware or persistence helpers in when they have a clearer home
- Do not create folders for simple leaf actions unless they are real controllers
Core Remix Rules
- Import from , never
import { ... } from 'remix'
- Treat as the source of truth for URLs. Use for
redirects, links, tests, and internal URL construction
- Controllers and actions should return explicit objects, including redirects, 404s, and
validation failures. At the route boundary, prefer returning a 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 layers interactivity on top
- Validate input at the boundary using (and
remix/data-schema/form-data
for
forms). makes the failure path a return value instead of an exception
- Derive from the root middleware stack so , ,
, and similar keys stay typed. If the controller never reads from context, it doesn't
need the harness
- Outside actions and controllers, only use when is in the
middleware stack
- Remix Component is not React: read props from , keep state in setup-scope
variables, call explicitly, and do DOM-sensitive work in event handlers or
, not in render
- Prefer host-element mixins via for behavior and styling instead of inventing
custom host prop conventions. Use only when composing multiple mixins
- Hydrated 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: always, by default, and when serving over
HTTPS
- Regenerate session IDs on login, logout, and privilege changes
- Use 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 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
- Build a fresh router per test or per suite so sessions, in-memory storage, and database state
stay isolated
- Use 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 ,
interact with the real DOM, and call 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 entry instead of a subpath
- Adding before the server-rendered route behavior is correct
- Passing non-serializable props into
- Calling without in the middleware stack
- Getting middleware order wrong; fast exits like static files belong early, request enrichment later
- Skipping boundary validation and trusting raw , 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 the route means to return rather than
throwing a custom subclass and catching it elsewhere
- Reaching for when a tamper-sensitive or server-managed per-browser fact really
wants . 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 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 , , or 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
- — the router itself. Use for , controller and middleware
types, and registering routes
remix/fetch-router/routes
— declarative route builders. Use for , , , ,
, , when defining
- — adapter from Node's module to a Fetch-style router. Use for
in
- — browser asset server. Use for when serving compiled
scripts and styles, getting public hrefs, and emitting preloads. Shared compiler options such as
, , , and live at the top level
- — typed header parsers and builders. Use when reading , , or
setting , , etc., instead of hand-formatting strings
- — . Use for the canonical "POST then redirect"
pattern and other location changes
- — . Use when you need an HTML from a string
or stream without rendering through
- — . Use when compressing one-off responses outside
the global middleware
- — file-download responses. Use for
Content-Disposition: attachment
responses
- — low-level URL matching and generation. Use when working with raw
patterns outside the router (custom matchers, scripts)
- — Fetch-based HTTP proxying. Use to forward a request to another origin; pass
when the upstream needs forwarded proto, host, and port
Data, Validation, and Persistence
- — schema builders for runtime validation. Use for and
to validate any input that crosses a trust boundary, and when validated output
should map to a different value or type
- — common check helpers (, , , etc.).
Use to compose into a schema
- — 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
— and for parsing directly. Use
in actions that read browser forms
- — typed tables and a interface. Use for , ,
when modeling persisted data
- ,
remix/data-table-postgres
, — adapters.
Use to back with a real engine. SQLite accepts Node, Bun, and compatible
synchronous clients with the shared / surface
remix/data-table/migrations
— migration authoring and runners. Use for ,
remix/data-table/migrations/node
— from disk. Use in startup scripts that
apply migrations
remix/data-table/operators
— query operators such as . Use when clauses
need set or comparison logic
Auth, Sessions, and Cookies
- — the object: , , , , . Use for
any per-browser state where tampering would be a bug (login, "I submitted this form already",
cart, flash messages)
- — . Use to wire a session cookie and
storage backend into the root middleware stack
- ,
remix/session/memory-storage
, remix/session/cookie-storage
—
storage backends. Use for single-process apps, for tests,
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
- — 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
- — 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
- — , , the context key. Use to
resolve identity into the request context and to gate routes
UI, Hydration, and Browser Behavior
- — the component runtime: components, core mixins, , , ,
navigation helpers, and . Use for app UI behavior
- — server rendering: , . Use in the
helper that returns HTML responses
- — animation APIs: , , ,
, , and
- — UI primitives, mixins, glyphs, and theme helpers. Import from
, , , etc.
- — component test rendering helpers such as
- — JSX transform target. Configured in , rarely
imported directly
- — escaped HTML template literals. Use when generating HTML outside the
component system (RSS feeds, email bodies, error pages)
- — backend-agnostic storage interface. Use as the type bound for
upload destinations
- ,
remix/file-storage/memory
, — storage
backends. Use to implement an upload destination
Middleware
- — . Use to serve files from exactly as
they exist on disk
remix/form-data-middleware
— . Use to parse once and expose it via
instead of calling in each action
- — lower-level , . Use when implementing
custom upload handlers. Upload handler errors propagate directly
- and
remix/multipart-parser/node
— low-level multipart stream parsing.
is a plain object keyed by lower-case header name; read values with
bracket notation such as part.headers['content-type']
remix/compression-middleware
— . Use globally for text-like responses
- — . Use in development for request logs; pass to
force terminal color output on or off
remix/method-override-middleware
— . Use when HTML forms need ,
, or
remix/async-context-middleware
— , . Use when helpers outside
actions need request context without threading it through every call
- — . Use for endpoints called cross-origin
- — . Use when session-backed forms mutate state and need
synchronizer-token CSRF protection
- — cross-origin protection. Use to reject unsafe cross-origin browser
requests
Test
- — , , and lifecycle hooks. Use as the test framework
- — programmatic test runner APIs such as
- — programmatic Remix CLI API. Use the executable for project commands such
as , , and
- — assertion helpers. Use in place of so messages render cleanly
in the runner
- — 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
typescript
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
typescript
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
typescript
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 })
Validate, mutate, and respond
typescript
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
for every outcome, and is ready for
interactivity when the UI needs it.
Build UI from handle props plus render
tsx
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
and
when the component needs browser interactivity or
browser-only APIs.