- 16 cross-shell utilities converted to sh/bash chezmoi executables: ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj (ghg-clone not named ghg: unrelated cargo binary owns that name) - cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files - fish keeps only shell-state functions; converted ones now hit PATH - cp-appid ignore gating moved to .local/bin path - dot_agents/skills: 82 skills materialized from dotstate storage so dot_claude skill symlinks resolve on fresh bootstrap; dotstate profile/symlinks.json released .agents/skills .bashrc .zshrc
10 KiB
Middleware and Server Setup
What This Covers
How to compose the request lifecycle and bridge the router to a runtime. Read this when the task involves:
- Choosing or ordering built-in middleware in the stack
- Writing custom middleware that sets typed context values
- Adding fast-exit handling (static files, CORS preflights) versus request-enriching layers (sessions, auth, data loading)
- Choosing when to keep the generated Node server versus switching server adapters
For data and persistence specifics, see data-and-validation.md. For session and auth specifics, see auth-and-sessions.md.
Middleware Stack
Middleware runs in order for every request. Place fast-exit middleware (static files) early and request-enriching middleware (session, auth) later.
Recommended ordering:
import { createRouter } from 'remix/router'
import { compression } from 'remix/middleware/compression'
import { formData } from 'remix/middleware/form-data'
import { logger } from 'remix/middleware/logger'
import { methodOverride } from 'remix/middleware/method-override'
import { session } from 'remix/middleware/session'
import { staticFiles } from 'remix/middleware/static'
import { asyncContext } from 'remix/middleware/async-context'
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 })
Built-in middleware catalog
| Middleware | Import | Use when | Notes |
|---|---|---|---|
staticFiles(dir, opts?) |
remix/middleware/static |
Serve files from public/ or another directory exactly as they exist on disk |
Fast exit; usually near the top |
compression() |
remix/middleware/compression |
Compress text-like responses | Usually app-wide |
logger() |
remix/middleware/logger |
Log requests and responses | Often development-only; colors can force color output on/off |
cors(opts?) |
remix/middleware/cors |
Endpoints must serve cross-origin browsers or preflight OPTIONS requests |
Usually early so preflights can short-circuit |
cop(opts?) |
remix/middleware/cop |
Reject unsafe cross-origin browser requests without synchronizer tokens | Put before session or CSRF when used |
formData(opts?) |
remix/middleware/form-data |
Parse FormData bodies, especially forms and uploads |
Needed for _csrf form field extraction |
methodOverride() |
remix/middleware/method-override |
HTML forms need PUT, PATCH, or DELETE semantics |
Run after form parsing |
session(cookie, storage) |
remix/middleware/session |
Cookie-backed sessions | Must run before session-backed auth or CSRF |
csrf(opts?) |
remix/middleware/csrf |
Session-backed form workflows need synchronizer-token CSRF protection | Requires session() before it |
asyncContext() |
remix/middleware/async-context |
Helpers outside handlers need request context via getContext() |
Add before helpers rely on it |
auth({ schemes }) |
remix/middleware/auth |
Resolve auth state into context.get(Auth) |
Run after session() for session-backed auth |
requireAuth() |
remix/middleware/auth |
A controller or action must reject anonymous access | Usually controller middleware or action middleware |
Static files vs browser modules
- Use
staticFiles()for files that should be served directly from disk, such as images, fonts, or already-built assets inpublic/ - Use
remix/assetswhen browser modules should be compiled and served from source files with import rewriting, preloads, or fingerprinted URLs
Ordering notes
- Put fast exits early:
staticFiles(),cors()preflight handling, andcop()when used - Parse request bodies before middleware that depends on them, such as
methodOverride()and form field token extraction incsrf() - Run
session()beforecsrf()and before session-backedauth() - Add
asyncContext()before helpers or shared code callgetContext() - Keep route protection like
requireAuth()as controller middleware or action middleware unless the entire app is private
Common stacks
- Session-backed HTML app ->
compression(),staticFiles(), optionalcop(),formData(),methodOverride(),session(), optionalcsrf(),asyncContext(),auth({ schemes }) - Cross-origin API ->
compression(),cors(), optionalasyncContext(), optionalauth({ schemes }) - Upload flow ->
compression(),staticFiles(),formData({ uploadHandler }), then sessions, auth, and data-loading middleware as needed
Middleware with options
// Static files with cache headers
staticFiles('./public', {
cacheControl: 'no-store, must-revalidate',
etag: false,
lastModified: false,
})
// Form data with upload handler
import type { FileUpload } from 'remix/form-data-parser'
import { createFsFileStorage } from 'remix/file-storage/fs'
let fileStorage = createFsFileStorage('./tmp/uploads')
formData({
uploadHandler(fileUpload: FileUpload) {
return fileStorage.set(fileUpload.name, fileUpload)
},
})
Errors thrown or rejected by uploadHandler propagate directly. Catch domain-specific upload errors at the route boundary when they should become user-facing Response objects.
Writing Custom Middleware
Middleware is a function that receives (context, next). Return a Response to short-circuit, call and return next() when you need the downstream response, or return nothing when you only set context and want the router to continue automatically.
Setting context values
Use context.set(key, value) to add typed values accessible downstream via context.get(key).
import type { Middleware } from 'remix/router'
import { Database } from 'remix/data-table'
export function loadDatabase(): Middleware {
return async (context, next) => {
context.set(Database, db)
return next()
}
}
Guarding routes
import { Auth } from 'remix/middleware/auth'
export function requireAdmin(): Middleware {
return (context, next) => {
let auth = context.get(Auth)
if (auth.identity?.role !== 'admin') {
return new Response('Forbidden', { status: 403 })
}
return next()
}
}
Async context for helpers
asyncContext() stores the request context in AsyncLocalStorage so helpers can reach it without the context being threaded through every call. Wrap getContext() in app-specific helpers:
// app/utils/context.ts
import { getContext } from 'remix/middleware/async-context'
import { Auth } from 'remix/middleware/auth'
import { Database } from 'remix/data-table'
import { Session } from 'remix/session'
export function getCurrentDb() {
return getContext().get(Database)
}
export function getCurrentSession() {
return getContext().get(Session)
}
export function getCurrentUser() {
let auth = getContext().get(Auth)
if (!auth.ok) {
throw new Error('Expected an authenticated user. Run requireAuth() before this code.')
}
return auth.identity
}
export function getCurrentUserSafely() {
let auth = getContext().get(Auth)
return auth.ok ? auth.identity : null
}
Middleware Types
Middleware has three API-owned forms:
-
Router middleware — runs for every request:
let router = createRouter({ middleware: [logger(), session(cookie, storage)] }) -
Controller middleware — runs for the direct actions in one controller:
export default createController(routes.account, { middleware: [requireAuth()], actions: { ... }, })Controller middleware does not flow into other controllers. Add the middleware to each controller that needs it.
-
Action middleware — runs for a single action:
router.get(routes.account.index, { middleware: [requireAuth()], handler(context) { return render(<AccountPage identity={context.auth.identity} />) }, })
Prefer inline arrays for middleware options. Use RouterContext<typeof router> to derive an app context from a router that uses inline middleware. Use createMiddleware() only when a chain is stored in a variable and its exact tuple type needs to be preserved, such as when deriving MiddlewareContext<typeof rootMiddleware> without a router value, exporting a reusable chain, or returning a chain from a factory.
Node Server Setup
New apps already include a server.ts that adapts the app router with remix/node-fetch-server. Keep that generated server unless the task specifically needs to change runtime behavior such as host/protocol handling, TLS, HTTP/2, WebSockets, deployment lifecycle, or test-only server setup.
Use remix/node-fetch-server when you want to keep owning a standard Node http, https, or http2 server directly.