@noverax/courier-remix serves the proxy that createProxyCaller and createProxyQueryCaller call: one route answers every endpoint, reads over GET and writes over POST, and streams server-sent events.
In one command
npx @noverax/create-courierIn a React Router or Remix app it adds what this page sets up by hand — the resource route, its entry in app/routes.ts, and Studio's plugin — with a typed example API, its mock, and a page at /courier. See Create Courier.
Install
npm install @noverax/courier-remix @noverax/courierRemix continues as React Router 7's framework mode, which is what npx create-react-router@latest sets up, and the handler works with both.
Usage
// app/routes/api.request.ts
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-remix"
import { sources } from "~/api"
registerSources(sources)
const handler = courierHandler()
export const loader = handler
export const action = handlerReact Router 7 serves only the routes app/routes.ts lists, so add the file there:
// app/routes.ts
import { type RouteConfig, index, route } from "@react-router/dev/routes"
export default [
index("routes/home.tsx"),
route("api/request", "routes/api.request.ts"),
] satisfies RouteConfigOn Remix v2, whose file-system routes serve app/routes/api.request.ts at /api/request without a config, the route file alone is enough.
courierHandler() is zero-arg — sources are read at request time from the core global registry that registerSources() fills when the route module loads. The handler uses Web Standard Request/Response natively — no conversion needed. loader answers GET requests, routed to query-type endpoints, and action answers POST, for mutations.
The proxy reads a JSON call up to its maxBodyBytes, 1 MB, and an upload (multipart/form-data) up to its maxUploadBytes, 10 MB; pass others to raise them: courierHandler({ maxUploadBytes: 50 * 1024 * 1024 }). In development, a client that declares an oversized upload can see the connection close before it has sent it all, rather than read the 413.
The client
Pair with createProxyCaller on the client:
import { createProxyCaller } from "@noverax/courier"
import type { api } from "./api"
// The first argument is the key this source has in your `sources` map —
// it is what the proxy addresses, and it travels as `?_s=<name>`.
const caller = createProxyCaller<typeof api>("api", {
proxyPath: { http: "/api/request" },
})
const users = await caller.users.getAll()The user's session
An API that keeps a session gets the user's session with every call a loader, an action or a step it awaits makes, once the request is served with it at hand. List the middleware in the root route:
import { requestContext } from "@noverax/courier-remix/request-context"
export const middleware = [requestContext()]React Router 8 runs route middleware as it is; React Router 7 runs it with future: { v8_middleware: true } in react-router.config.ts. The session cookies the API sets reach the browser on the response, a document's or a data request's. The request is kept in AsyncLocalStorage, which Node provides, and Cloudflare with the nodejs_compat flag; the main entry imports none of it.
react-router-serve hands React Router a Request alone, so forward's client address is not sent. An Express server of your own tells it: serve the requests with requestContext from @noverax/courier-express instead. A server of your own that calls the handler createRequestHandler makes — Bun's, Deno's, a Worker's — wraps it:
import { createRequestHandler } from "react-router"
import { withRequestContext } from "@noverax/courier-remix/request-context"
const handle = withRequestContext(createRequestHandler(build, "production"))Remix v2 has no middleware. Serve its handler from an Express server of your own — the one its Express template starts with — and mount requestContext from @noverax/courier-express before it:
import { requestContext } from "@noverax/courier-express"
import { createRequestHandler } from "@remix-run/express"
app.use(requestContext())
app.all("*", createRequestHandler({ build }))React Router 7 and 8 are checked with react-router-serve, and Remix v2 with its Express template's server, compression included. See User Sessions.
Cancelled calls
When the browser cancels a call — an aborted upload, a query it no longer needs — the proxy stops its own upstream request, following the request's signal. React Router 7 hands a resource route a rebuilt copy of the request, and the copy's signal does not reliably follow the original: a cancelled upload to a slow upstream was seen to run to completion on both react-router dev and react-router-serve. The future flag that passes the request through unchanged keeps the cancellation:
// react-router.config.ts
export default {
future: { v8_passThroughRequests: true },
}