Express backend
このコンテンツはまだ日本語訳がありません。
ExpressBackend lets you run actor-ts routes through
Express — the most-widely-used Node HTTP framework. Right
choice when:
- You already have Express middleware invested (custom auth, session handling, app-specific instrumentation).
- Team familiarity with Express > framework benefits of Fastify.
- You’re incrementally migrating an existing Express app to actor-ts.
import { ActorSystem } from 'actor-ts';import { HttpExtensionId } from 'actor-ts/http';import { ExpressBackend, ExpressBackendOptions } from 'actor-ts/http';
const http = system.extension(HttpExtensionId);
await http.newServerAt('0.0.0.0', 8080) .useBackend(new ExpressBackend()) .bind(routes);Configuration
Section titled “Configuration”const expressBackendOptions = ExpressBackendOptions.create().withMaxBodyBytes(4 * 1024 * 1024);new ExpressBackend( expressBackendOptions,);Express-style settings. maxBodyBytes defaults to 1 MiB, the shared
cap every backend applies — raise it only for an endpoint that genuinely
takes more, as above.
Behind a reverse proxy
Section titled “Behind a reverse proxy”req.ip is the socket peer, which is what lands in
HttpRequest.remoteAddress. Express can be told to derive it from
X-Forwarded-For instead — but only on a bring-your-own app
(app.set(...), passed via .withApp(app)), because
ExpressBackendOptions has no trustProxy field.
Prefer IpAllowlist’s trustedProxies
over that route. It is the same trust-by-address rule, it does not need a
bring-your-own app, and it reads identically on all three backends —
Hono has no trust-proxy concept at all.
Security headers
Section titled “Security headers”Every response this backend writes carries
X-Content-Type-Options: nosniff — its error mapping, the fallback 404
and the body-too-large 413 included, none of which a middleware sees.
A response’s own header still wins.
Configure it per server, not per backend — one surface instead of three:
await http.newServerAt('0.0.0.0', 8080) .useBackend(new ExpressBackend()) .withSecurityHeaders(false) // or a SecurityHeadersOptions bundle .bind(routes);Adding Express middleware
Section titled “Adding Express middleware”import express from 'express';import { ExpressBackend } from 'actor-ts/http';
const backend = new ExpressBackend();await http.newServerAt('0.0.0.0', 8080) .useBackend(backend) .bind(routes);
// Access the raw Express app:backend.getApp().use(express.session({ secret: '...' }));backend.getApp().use(expressRateLimitFromNpm);backend.getApp().use(customAuth);Express middleware wraps the actor-ts routes — request flows through your middleware first, then to the actor-ts handler.
This is the main reason to pick Express over Fastify: the middleware ecosystem. If you don’t need it, Fastify is faster.
WebSocket handshakes go through it too
Section titled “WebSocket handshakes go through it too”A websocket() route is registered as an ordinary Express GET,
and the upgrade is dispatched through the app — so app.use(...)
runs for the handshake exactly as it runs for a request:
const backend = new ExpressBackend();backend.getApp().use(customAuth); // gates /ws as well
await http.newServerAt('0.0.0.0', 8080) .useBackend(backend) .bind(websocket('/ws', chat));A middleware that answers the request (res.status(401).end())
cancels the handshake: the socket is closed instead of upgraded and
onClientConnected never fires. A middleware that calls next()
lets it through to the framework’s own upgrade guard
(withMiddleware() / allowedOrigins), which still has the last
word — see WebSockets.
Two caveats on this path. A middleware that neither answers nor
calls next() leaves the client waiting, the same way it would
stall an ordinary request. And a rejection’s body only reaches
the client on runtimes that let a hijacked upgrade socket be written
to — Bun and Node do, Deno does not, where the client sees the
connection close instead. Either way the handshake is refused.
import express from 'express';import https from 'node:https';
// TLS is set up on a bring-your-own Express app, wrapped in Node's// `https` server; pass the app to the backend via `.withApp(app)`.const app = express();https.createServer( { cert: fs.readFileSync('./tls/cert.pem'), key: fs.readFileSync('./tls/key.pem'), }, app,);
const expressBackendOptions = ExpressBackendOptions.create().withApp(app);new ExpressBackend(expressBackendOptions);Backed by Node’s https module. Same caveats as the Fastify
backend — typically TLS terminates at the load balancer.
Peer dependency
Section titled “Peer dependency”npm install express# or: bun add expressFor Express 5+ recommendation; older versions may work but aren’t tested.
Performance
Section titled “Performance”Rough numbers:
- 40K-60K req/sec for trivial routes (slower than Fastify).
- P50 latency similar; throughput differs.
Express’s middleware chain has more overhead than Fastify’s hooks. For high-throughput paths, prefer Fastify; for paths gated by heavy middleware, the framework choice doesn’t matter much.
Migrating from existing Express app
Section titled “Migrating from existing Express app”If you have an existing Express app and want to add actor-ts:
import express from 'express';import { ExpressBackend, ExpressBackendOptions } from 'actor-ts/http';
const app = express();
// Existing routes + middleware stay as-is:app.use('/legacy', oldLegacyRouter);
// Hand the existing app to the backend; actor-ts registers its// routes on the same app, alongside your existing ones.const expressBackendOptions = ExpressBackendOptions.create().withApp(app);const backend = new ExpressBackend(expressBackendOptions);
await http.newServerAt('0.0.0.0', 8080) .useBackend(backend) .bind(routes);Pass your existing app to the backend via .withApp(app) —
actor-ts registers its routes on the same app, so your existing
routes and middleware keep working without swapping the whole
HTTP stack.
Where to next
Section titled “Where to next”- HTTP overview — the bigger picture.
- Fastify backend — the default + recommended.
- Route DSL — what backends register.
