Files
dotfiles/dot_agents/skills/remix/references/middleware-and-server.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 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
2026-08-10 15:15:06 -04:00

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 in public/
  • Use remix/assets when 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, and cop() when used
  • Parse request bodies before middleware that depends on them, such as methodOverride() and form field token extraction in csrf()
  • Run session() before csrf() and before session-backed auth()
  • Add asyncContext() before helpers or shared code call getContext()
  • 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(), optional cop(), formData(), methodOverride(), session(), optional csrf(), asyncContext(), auth({ schemes })
  • Cross-origin API -> compression(), cors(), optional asyncContext(), optional auth({ 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:

  1. Router middleware — runs for every request:

    let router = createRouter({ middleware: [logger(), session(cookie, storage)] })
    
  2. 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.

  3. 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.