Skip to content

Express

@cratis/arc.express adds one middleware to an Express 5 application. Arc answers its own routes; everything else goes to your routes and middleware.

server.ts
import express from 'express';
import { cratisArc } from '@cratis/arc.express';
import { arc } from './arc.js';
const app = express();
const middleware = cratisArc(arc);
app.use(middleware);
app.use(express.json());
app.get('/health', (_request, response) => { response.send('ok'); });
const listener = app.listen(3000, '127.0.0.1');
middleware.injectWebSocket(listener);
process.once('SIGTERM', () => {
void middleware.shutdown(listener);
});

arc is the built application from Host adapters. middleware.shutdown(listener) coordinates participant stop and drain with transport closure. If you supplied your own ServiceRegistry, pass it explicitly as middleware.shutdown(listener, registry); Arc never implicitly disposes a borrowed registry. With the Tasks sample’s artifacts, POST /api/tasks/registration/register-task now reaches Arc, and GET /health reaches your route.

The middleware compares the raw request path, before any normalization, with Arc’s registered routes. Only an exact match is handled by Arc. Every other request goes to next(), including spellings such as //api/echo, /x/../api/echo, or /api/%2e%2e/api/echo that would only match after normalization. The adapter builds the Fetch API request on a fixed internal origin and never uses the Host header for routing.

An unexpected error inside the adapter is passed to Express with next(error). An unknown path gets Express’s own 404, which carries no Arc correlation header.

context.signal aborts when the client disconnects before the response finishes.

cratisArc(arc, native) accepts a second argument: a callback returning trusted native context for each request. Use it with nativePrincipal: true to pass a user your Express session or JWT middleware already verified. See Native principal.

Express HTTP middleware does not run on Node upgrade requests. Attach WebSockets to the listener with middleware.injectWebSocket(listener, native?); the middleware cannot see upgrades. Call middleware.shutdown(listener) to close the listener and Arc in the required order. middleware.close(listener) remains listener-only; if participants are registered, closing the listener first can start scope disposal before they stop. See WebSockets.