@signmax/remix-base
v1.0.0
Published
Server and shared utils for React Router 8 projects
Readme
@signmax/remix-base
Production-ready server and shared utilities for React Router 8 apps.
@signmax/remix-base bundles an opinionated Express 5 setup, middleware suite, instrumentation, and utilities so you
can ship React Router 8 projects without re-writing the same server glue.
Requirements
- React Router
^8.0.0(react-routerand@react-router/express) — middleware andRouterContextProviderare unconditional in v8, so no future flags are needed - Node
>=22.22.0, the floor React Router 8 sets - Express
^5.0.0
Quick Start
npm install @signmax/remix-baseimport { serveApp } from "@signmax/remix-base/server"
import type { ServerBuild } from "react-router"
const build = () => import("./build/server/index.js") as Promise<ServerBuild>
await serveApp(build, {})What You Get
- Express 5 server with sensible defaults and CloudFront-aware proxy trust
- Middleware bundle (Helmet, CSP, no-index, trailing slash guard) plus optional device-key helper
- Built-in Sentry scope middleware that enriches every request with filtered params, tags, and user IP
- Structured logging via Pino and Prometheus metrics endpoint
- GraphQL helpers with cookie pass-through and shared-secret auth
- React Router 8 server context (
serverContext) and optional GrowthBook context - AWS Secrets Manager helper
- TypeScript-first API with comprehensive tests
Server Setup
Switch to the options object when you need control over middleware, dev servers, or context creation:
import { serveApp, createDefaultMiddleware, type ServeAppOptions } from "@signmax/remix-base/server"
import { getServerContext } from "@signmax/remix-base/router_context"
import { deviceKeyMiddleware, requestMiddleware } from "@signmax/remix-base/middleware"
const build = async () => import("../build/server/index.js")
const options: ServeAppOptions = {
middleware: [...createDefaultMiddleware(), requestMiddleware(), deviceKeyMiddleware({ cookieName: "device_id" })],
getLoadContext: (req, res) => getServerContext(req, res),
trustCloudFrontIPs: true,
}
await serveApp(build, options)Passing middleware replaces the defaults entirely, so spread createDefaultMiddleware() (trailing slash, Helmet, CSP)
when you still want them. serveApp always wires in the Sentry scope middleware, Pino request logger, compression,
cookie parser, and a /livez health endpoint on top.
In development, pass a Vite dev server (devServer: viteServer.middlewares) and call startMetrics() when you want a
Prometheus endpoint.
Router Context
@signmax/remix-base/router_context plugs into React Router's middleware context. getServerContext returns a
RouterContextProvider seeded with the per-request serverContext value (revision, logger, IP, CSP nonce, and the
GraphQL request function) — the shape React Router 8 requires every getLoadContext to return.
import { serverContext } from "@signmax/remix-base/router_context"
import type { LoaderFunctionArgs } from "react-router"
export const loader = async ({ context }: LoaderFunctionArgs) => {
const { gqlRequest, log, cspNonce } = context.get(serverContext)
log.info("loading dashboard")
const data = await gqlRequest(/* ... */)
return { data, cspNonce }
}The gqlRequest function is installed on req.request by requestMiddleware and surfaced through the context as
gqlRequest.
Middleware & Utilities
import {
cspMiddleware,
deviceKeyMiddleware,
endingSlashMiddleware,
helmetMiddleware,
noIndexMiddleware,
requestMiddleware,
} from "@signmax/remix-base/middleware"
import {
BrowserDetection,
pipeHeaders,
getConservativeCacheControl,
makeTimings,
time,
getRevision,
} from "@signmax/remix-base/util"cspMiddlewareseedsres.locals.cspNoncewith a fresh nonce on every request.requestMiddlewareis a factory that accepts GraphQL client options and returns middleware that attaches a GraphQL request helper toreq.request:import { requestMiddleware } from "@signmax/remix-base/middleware" const customRequestMiddleware = requestMiddleware({ endpoint: "https://api.example.com/graphql", sharedSecret: process.env.SHARED_SECRET, sharedSecretHeader: "x-api-key", passthroughHeaders: ["x-tenant-id"], skipCookies: false, }) // Or use with default options const defaultRequestMiddleware = requestMiddleware()Utility exports cover HTTP headers, server timing, revision lookup, and user-agent parsing helpers.
Logging & Metrics
import logger from "@signmax/remix-base/logger"
import { startMetrics } from "@signmax/remix-base/metrics"
logger.info("Application started")
const metrics = await startMetrics({ port: 9394 })startMetrics spins up a dedicated Express app exposing /metrics and returns a handle so you can stop the server
during shutdown.
GraphQL Client
import { createClient, createRequest, createResponseMiddleware } from "@signmax/remix-base/client"
const requestFn = createRequest(req, res, createResponseMiddleware(req, res), {
endpoint: "https://api.example.com/graphql",
sharedSecret: process.env.SHARED_SECRET,
passthroughHeaders: ["x-tenant-id"],
})Set includeDefaultPassthroughHeaders to false when you want complete control over forwarded headers.
AWS Secrets Manager
import { loadSecrets } from "@signmax/remix-base/secrets"
const secrets = await loadSecrets<{ apiKey: string }>("my-app/production", {
region: "us-east-1",
})The region falls back to AWS_REGION or eu-central-1.
Optional Integrations
Sentry – peer dependencies
@sentry/react-routerand@sentry/profiling-nodemust be installed. Callinitfrom your instrumentation entry (--import ./instrument.mjs) whenSENTRY_DSNis set. The helper enables Sentry only inproduction/stagingenvironments, wires Pino + HTTP + profiling integrations, and filters health-check traffic.import { init } from "@signmax/remix-base/instrumentation" if (process.env.SENTRY_DSN) { init({ dsn: process.env.SENTRY_DSN, environment: "production", tracesSampleRate: 0.1, }) }Every request is automatically enriched with a Sentry isolation scope (method, path, host, IP, referrer, user-agent, filtered query/body params, and response status) via the built-in
sentryScopeMiddleware.GrowthBook – install
@growthbook/growthbook+eventsourceand expose a scoped client through React Router's context.import { createGrowthBook, createScopedGrowthBook, growthbookContext } from "@signmax/remix-base/growthbook" import { getServerContext } from "@signmax/remix-base/router_context" const growthbook = await createGrowthBook({ apiHost: "https://cdn.growthbook.io", clientKey: "key" }) const getLoadContext = async (req, res) => { const context = getServerContext(req, res) context.set(growthbookContext, await createScopedGrowthBook(req, growthbook)) return context }Pass a
stickyBucketService(an implementation of the SDK'sStickyBucketService) to persist experiment assignments across sessions. Bot requests skip the service entirely, and store failures fail open to hash-based assignment. If identity attributes only become known mid-request (e.g. after auth middleware), callrefreshStickyBuckets(gbInstance, growthbook, stickyBucketService)afterupdateAttributesto load the assignments keyed on the new attributes.
Environment Variables
NODE_ENV– sets development/production modePORT– HTTP port (4000by default)BUILD_DIR– static asset root (build/clientby default)ASSETS_DIR– fingerprinted assets directory (defaults to${BUILD_DIR}/assets)PROMETHEUS_EXPORTER_PORT– metrics server port (9394by default)AWS_REGION– Secrets Manager region (eu-central-1by default)SENTRY_DSN– enables Sentry when set alongside aproduction/stagingenvironmentGIT_REV– optional release/commit override for logging and Sentry
Testing Helpers
import { gqlOpHandler } from "@signmax/remix-base/test/helpers"MSW helpers simplify GraphQL mocking and reuse the package defaults.
Publishing
Releases are driven by GitHub Releases:
- Update the version in
package.jsonand refreshCHANGELOG.md. - Commit, push, and tag (
vX.Y.Z). - Create a GitHub Release; CI publishes to npm.
License
MIT
