Build and review Remix 3 applications using the `remix` npm package and subpath imports. Use when working on Remix app structure, routes, controllers, middleware, validation, data access, auth, sessions, file uploads, server setup, UI components, hydration, HMR, navigation, or tests.
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Use this skill for end-to-end Remix app work. This skill helps you choose the right layer first, reach for the right package, and avoid the most common Remix-specific mistakes.
This skill is the quick guide. When you need fuller API documentation, examples, or package-specific details for a remix/* subpath, first look for a README next to the relevant generated source file in the published remix package: node_modules/remix/src/<subpath>/README.md. These published README files are generated mirrors; in the Remix source repository, the canonical README lives in the owning packages/* package and the packages/remix/src/**/README.md mirrors are intentionally ignored. If that README does not exist, look for the nearest parent README because some subpaths share their parent package documentation.
Examples:
remix/router -> node_modules/remix/src/fetch-router/README.mdremix/ui/button -> node_modules/remix/src/ui/button/README.mdRemix 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:
app/routes.ts define the typed URL contract and power href() generation.app/actions implement that contract and return Response objects.context.set(Key, value).remix/ui. This is not React. A component receives a handle, reads current props from handle.props, and returns a zero-argument render function.Use this skill for:
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, development HMR | references/middleware-and-server.md |
| Compiling and serving browser modules, asset URL namespaces, preloads, browser HMR | 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, browser HMR update handling, <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:
app/routes.ts before wiring handlers or UI.Response via router.fetch(...) before you add clientEntry(...), animations, or DOM effects.AppContext from the middleware stack and use it in controllers.Request, FormData, params, cookies, and external payloads before they reach rendering or persistence logic.clientEntry(...) and run(...) only for real browser interactivity or browser-only APIs.Use these root directories consistently:
app/ for runtime application codedb/ for migrations and local database filespublic/ for static assets served as-is from the app roottest/ for shared helpers, fixtures, and integration coveragetmp/ for uploads, caches, local session files, and other scratch dataInside app/, organize by responsibility:
actions/ for controller-owned route handlers, route-local response rendering, and route-local UI/helpers that are not shared across route areasdata/ for schema, queries, persistence setup, migrations, and runtime data initializationmiddleware/ for request lifecycle concerns such as auth, sessions, uploads, and database injectionpublic/ directories inside the narrowest owner for browser-reachable source code, with the browser runtime entrypoint at app/actions/public/entry.tsui/ for shared cross-route UI primitivesutils/ only for genuinely cross-layer helpers that do not clearly belong elsewhereroutes.ts for the shared server-and-browser route contract and type-safe href generationrouter.ts for router setup and wiringWhen code could live in multiple places:
app/ui/.app/middleware/.app/data/.app/utils/ only as a last resort for truly cross-layer helpers.app/actions/controller.tsxactions object contains only direct leaf route keys from the route map passed to router.map(...)app/actions/<route-key>/controller.tsx for each nested route map that needs actions or controller middleware, and map it explicitly with router.map(routes.<routeKey>, controller)app/actions/ after route-map keys, not URL path segmentsapp/ui/app/router.ts to map that route map explicitlyrender() from remix/middleware/render in the router middleware stack for normal Remix UI applications. Pass render({ assets }) when source-based clientEntry() modules need browser URLscontext.render(node, init). Status, headers, and other response policy remain explicit in the actionrenderWith(...), renderToStream(...), and createHtmlResponse(...) only when an application intentionally owns a custom renderer contract or replaces the standard UI response pipelineapp/utils/<topic>.ts modules. Formatting, MIME classification, path parsing, sorting, and normalization should be testable without a router, request context, or Response, and should not import from app/actions, remix/ui/server, or remix/response/*render(...); keep response assembly in actions and extract only the pure helpersapp/lib/ as a generic dumping groundapp/components/ as a second shared UI bucket when app/ui/ already owns that roleapp/controllers/; Remix app route handlers live under app/actions/app/actions/app/actions/controller.tsxactionsapp/router.ts when they belong in a controllerapp/utils/ when they have a clearer homeremix/<subpath>, never import { ... } from 'remix'app/routes.ts as the source of truth for URLs. Use routes.<name>.href(...) for redirects, links, tests, and internal URL constructionResponse 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 flowrouter.map(routes, controller) maps only the direct leaf routes in routes; nested route maps must be mapped with their own explicit controllersclientEntry(...) layers interactivity on topremix/data-schema (and remix/data-schema/form-data for forms). parseSafe makes the failure path a return value instead of an exceptionAppContext from the middleware stack so get(databaseContext), get(Session), get(Auth), and similar keys stay typed. If the controller never reads from context, it doesn't need the harnessgetContext() when asyncContext() is in the middleware stackfunction Name(handle: Handle<Props>) { return () => ... }, 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 rendermix={mixin(...)} for behavior and styling instead of inventing custom host prop conventions. Use mix={[...]} only when composing multiple mixinsmix={css(...)}. Extract a module-scoped style descriptor when it forms a reused visual recipe, has substantial selectors, media queries, or keyframes, or is large enough to obscure the component. Export a style descriptor only after multiple modules need the same visual recipe; otherwise keep it with its narrowest ownerclientEntry(...) props must be serializable. Do not pass functions, class instances, or opaque runtime objectshttpOnly always, sameSite by default, and secure when serving over HTTPSrequireAuth() to protect authenticated route areas, but still authorize resource ownership inside handlers and data writesremix/html-template for HTML generation so escaping stays correctrouter.fetch(new Request(...)) and assert on the returned Responseapp/actions/controller.test.ts(x), and nested route-map behavior belongs beside that route-key controllerroutes.<name>.href(...) in tests so URLs stay coupled to the route contractcreateMemorySessionStorage() instead of production storageapp/utils helpers beside their modules. Test response behavior through router or controller testscreateRoot(...), interact with the real DOM, and call root.flush() between stepsremix entry instead of a subpathclientEntry(...) before the server-rendered route behavior is correctclientEntry(...)getContext() without asyncContext() in the middleware stackFormData, params, cookies, or external payloadsResponse the route means to return rather than throwing a custom Error subclass and catching it elsewherecreateCookie 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<Frame> reloads as mutually exclusive patterns. Pick the lightest sync mechanism that fits the UX; small widgets may reasonably poll a JSON endpointutils.ts, helpers.ts, or common.ts when ownership is knownapp/controllers or standalone root action file layout instead of using controllers under app/actionsactions object. Map nested route maps explicitly in app/router.tsrouter.get(...)/router.post(...) registrations as the default app structure instead of using controllersUse 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.
remix/router — the router itself. Use for createRouter, controllers, middleware types, and registering routesremix/routes — declarative route builders. Use for route, get, post, put, del, form, resources when defining app/routes.tsremix/node-fetch-server — default Node adapter for new apps. Use createRequestListener with node:http, node:https, or node:http2 in server.ts when booting the template-style appremix/node-hmr — optional development Node HMR runner for rapid UI edits. Use run in hmr.ts to supervise server.ts behind an hmr script, and use createHmrReadyFetch when a stable public proxy should wait for child server readiness during updatesremix/node-hmr/runtime — child-process runtime API for code running under remix/node-hmr. Use to create browser HMR channels for asset servers and to emit server readiness after the child server starts listeningremix/node-hmr/types — type-only entry for import.meta.hot in Node modulesremix/assets — browser asset server. Use for createAssetServer when serving compiled scripts and styles, getting public hrefs, emitting preloads, and wiring browser HMR. Configure a basePath; use optional directory mounts configuration when the default mounts that serve app at app and node_modules at npm are not enough; use allowFiles/denyFiles for path and glob rules; and use exact package names in allowPackages for package-level access. Shared compiler options such as target, sourceMaps, sourceMapSourcePaths, and minify live at the top levelremix/assets/types/hmr — type-only entry for import.meta.hot in browser modules compiled by remix/assetsremix/headers — SuperHeaders plus typed header parsers and builders. Use the default export when you want a Headers subclass with typed accessors like headers.contentType, headers.cacheControl, and headers.setCookie; use named classes such as CacheControl, ContentDisposition, and Vary when working with individual header valuesremix/response/redirect — redirect(href, status?). Use for the canonical "POST then redirect" pattern and other location changesremix/response/html — createHtmlResponse. Use when you need an HTML Response from a string or stream without rendering through remix/uiremix/response/compress — compressResponse. Use when compressing one-off responses outside compression() middlewareremix/response/file — file-download responses. Use for Content-Disposition: attachment responsesremix/route-pattern — low-level URL matching and generation. Use RoutePattern or createMatcher when working with raw patterns outside the router. href(...) encodes pathname and search params for you, and match(...) returns decoded paramsremix/route-pattern/specificity — pattern ranking helpers. Use only when building custom matcher or reporting logic outside the normal router/matcher APIsremix/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. It also rewrites proxied Set-Cookie domain/path attributes by defaultremix/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 typeremix/data-schema/checks — common check helpers (email, minLength, maxLength, etc.). Use to compose into a schemaremix/data-schema/coerce — coercion helpers for strings, numbers, booleans, dates, and ids. Use when input arrives as a string but should be a typed valueremix/data-schema/form-data — f.object and f.field for parsing FormData directly. Use in actions that read browser formsremix/data-schema/lazy — recursive or mutually-referential schemas. Use when a schema needs to refer to itself or another schema that is declared laterremix/data-table — typed tables and the shared Database API. Use table and column when modeling persisted data, then create a concrete database from the matching dialect package. Integration packages can implement DatabaseDriver and extend Database to add another SQL dialectremix/data-table/sqlite, remix/data-table/postgres, remix/data-table/mysql — concrete database integrations. Use createSqliteDatabase, createPostgresDatabase, or createMysqlDatabase. SQLite accepts Node, Bun, and compatible synchronous clients with the shared prepare/exec surfaceremix/data-table/migrations — migration authoring and registries. Use for createMigration and createMigrationRegistry; run migrations with Database.migrate()remix/data-table/migrations/node — loadMigrations from disk. Use in startup scripts that apply migrationsremix/data-table/operators — query operators such as inList(...). Use when where clauses need set or comparison logicremix/data-table/sql-helpers — SQL helper utilities for database integrations or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query APIremix/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/middleware/session — session(cookie, storage). Use to wire a session cookie and storage backend into the middleware stackremix/session-storage/fs, remix/session-storage/memory, remix/session-storage/cookie — storage backends. Use fs-storage for single-process apps, memory-storage for tests, cookie-storage for stateless deployments where data fits in a cookieremix/session-storage/redis — Redis-backed storage. Use for multi-process or multi-host deploymentsremix/session-storage/memcache — Memcache-backed storage. Same multi-host use case as Redisremix/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/sessionremix/auth — credentials, OAuth, and OIDC providers. Use to define how identity is verified, start/finish external login, and refresh stored OAuth/OIDC token bundles with refreshExternalAuth(...)remix/middleware/auth — auth({ schemes }), requireAuth, the Auth context key. Use to resolve identity into the request context and to gate routesremix/ui — the component runtime: components, core mixins, clientEntry, run, <Frame>, navigation helpers, and createRoot. Use for app UI behaviorremix/ui/server — low-level server rendering with renderToStream and renderToString. Normal apps should install render() from remix/middleware/render; use this subpath for custom pipelines and static string renderingremix/ui-hmr — direct Remix UI component HMR transforms. Use only when writing a custom module hook or build integrationremix/ui-hmr/node — Node import hook for Remix UI component HMR. Use with --import remix/ui-hmr/node in development servers that run through remix/node-hmrremix/ui-hmr/assets — remix/assets loader for Remix UI component HMR. Use uiHmr() in createAssetServer({ scripts: { loaders } }) during developmentremix/ui/dev/refresh — development refresh support used by HMR tooling, not normal application coderemix/ui/animation — animation APIs: animateEntrance, animateExit, animateLayout, spring, tween, and easingsremix/ui/<primitive> — UI primitives, mixins, and component helpers. Current subpaths include remix/ui/accordion, remix/ui/anchor, remix/ui/button, remix/ui/checkbox, remix/ui/combobox, remix/ui/input, remix/ui/listbox, remix/ui/menu, remix/ui/popover, and remix/ui/selectremix/ui/test — component test rendering helpers such as renderremix/ui/jsx-runtime and remix/ui/jsx-dev-runtime — JSX transform targets. Configured in tsconfig.json, rarely imported directlyremix/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 destinationsremix/file-storage/fs, remix/file-storage/memory, remix/file-storage/s3 — storage backends. Use to implement an upload destinationremix/middleware/render — render({ assets?, onError? }) for the standard Remix UI renderer and renderWith(factory) for custom request-scoped renderers. Normal UI actions return context.render(node, init)remix/middleware/static — staticFiles(dir). Use to serve files from public/ exactly as they exist on diskremix/middleware/form-data — formData(). Use to parse FormData once and expose it via get(FormData) instead of calling await request.formData() in each actionremix/form-data-parser — lower-level parseFormData, FileUpload. Use when implementing custom upload handlers. Upload handler errors propagate directlyremix/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/middleware/compression — compression(). Use for text-like responsesremix/middleware/logger — logger(). Use in development for request logs; pass colors to force terminal color output on or offremix/middleware/method-override — methodOverride(). Use when HTML forms need PUT, PATCH, or DELETEremix/middleware/async-context — asyncContext(), getContext(). Use when helpers outside actions need request context without threading it through every callremix/middleware/cors — cors(opts?). Use for endpoints called cross-originremix/middleware/csrf — csrf(opts?). Use when session-backed forms mutate state and need synchronizer-token CSRF protectionremix/middleware/cop — cross-origin protection. Use to reject unsafe cross-origin browser requestsremix/test — describe, it, and lifecycle hooks. Use as the test frameworkremix/test/cli — programmatic test runner APIs such as runRemixTestremix/node-fetch-server/test — createTestServer for end-to-end tests that need a real local HTTP server around a Fetch handlerremix/cli — programmatic Remix CLI API. Use the remix executable for project commands such as remix test, remix routes, remix doctor, and remix versionremix/assert — assertion helpers. Use in place of node:assert so messages render cleanly in the runnerremix/terminal — ANSI styles, color detection, style factories, and testable terminal streams. Use for CLIs and terminal output instead of hand-rolled escape sequencesremix/fs — small filesystem helpers such as openLazyFile and writeFile. Use in Node-only app or tooling code when you need lazy file responses or safe file writesremix/lazy-file — LazyFile primitives and byte-range helpers. Use when implementing file or range responses below the higher-level response/file helpersremix/mime — content-type and MIME detection helpers. Use instead of maintaining app-local extension mapsremix/tar-parser — streaming tar parsing. Use for import/export tooling that consumes tar archivesimport { form, get, post, resources, route } from 'remix/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' }),
}),
})import { createController } from 'remix/router'
import { databaseContext } from '../middleware/database.ts'
import { routes } from '../routes.ts'
export default createController(routes.books, {
actions: {
async index({ get }) {
let db = get(databaseContext)
let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<BooksIndexPage allBooks={allBooks} />)
},
async show({ get, params }) {
let db = get(databaseContext)
let book = await db.findOne(books, { where: { slug: params.slug } })
if (!book) return new Response('Not Found', { status: 404 })
return render(<BookShowPage book={book} />)
},
},
})import { createRouter } from 'remix/router'
import rootController from './actions/controller.tsx'
import adminController from './actions/admin/controller.tsx'
import adminBooksController from './actions/admin/books/controller.tsx'
import authController from './actions/auth/controller.tsx'
import authLoginController from './actions/auth/login/controller.tsx'
import booksController from './actions/books/controller.tsx'
import contactController from './actions/contact/controller.tsx'
import { routes } from './routes.ts'
export const router = createRouter({ middleware })
router.map(routes, rootController)
router.map(routes.contact, contactController)
router.map(routes.books, booksController)
router.map(routes.auth, authController)
router.map(routes.auth.login, authLoginController)
router.map(routes.admin, adminController)
router.map(routes.admin.books, adminBooksController)import { createRouter } from 'remix/router'
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 })import { createController } from 'remix/router'
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 { databaseContext } from '../middleware/database.ts'
import { routes } from '../routes.ts'
let bookSchema = f.object({
slug: f.field(s.string()),
title: f.field(s.string()),
})
export default createController(routes.books, {
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(databaseContext)
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 }))
},
},
})This shape works without JavaScript, returns a Response for every outcome, and is ready for clientEntry(...) interactivity when the UI needs it.
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.
TanStack/ai
e8546f9
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.