콘텐츠로 이동
한국어

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);
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.

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.

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);

See Security best practices.

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.

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.

Terminal window
npm install express
# or: bun add express

For Express 5+ recommendation; older versions may work but aren’t tested.

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.

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.