Skip to Content
Getting StartedMigrateHTTP APIs (Express)

HTTP APIs (Express)

Express is the most common thing to migrate, and it works. We tested Express 4.22.3 and 5.1.0 with a JSON endpoint:

server.js
const express = require("express"); const app = express(); app.get("/", (req, res) => res.json({ express: require("express/package.json").version })); const s = app.listen(process.env.PORT || 3456, "127.0.0.1", () => console.log("up", s.address().port));
3va run server.js --allow-read=./node_modules --allow-net=127.0.0.1
up 3456 $ curl 127.0.0.1:3456 {"express":"5.1.0"}

Permissions an API usually needs

NeedGrant
Load express and its dependencies--allow-read=./node_modules
Listen on a portany --allow-net grant for loopback; 0.0.0.0 for every interface (see below)
Call another service--allow-net=api.stripe.com (exact host)
Connect to the database--allow-net=db.internal
Read PORT, DATABASE_URL, secrets--allow-env=PORT,DATABASE_URL
Serve static files, uploads--allow-read=./public, --allow-write=./uploads

Put them in package.json under "3va" so 3va run, 3va start and 3va test all share them (Permissions).

Things to keep in mind

  • Listening on loopback needs any network grant. Outbound calls are matched strictly against the host.

  • Every interface needs an explicit grant. app.listen(3000) asks for 0.0.0.0; unless --allow-net includes 0.0.0.0, :: or *, 3va binds 127.0.0.1 instead and prints a note. In a container, where the server must be reachable from outside, add --allow-net=0.0.0.0.

  • Without a grant the server doesn’t start, and the process still exits with code 0. The EACCES goes to the server’s error event. 3va now lists the refusal on exit:

    [!] 1 permission was denied during this run: bind a local server on 0.0.0.0 needs any --allow-net grant (e.g. --allow-net=127.0.0.1)

    Still add server.on("error", …) so your own logs show it too.

  • Ports in --allow-net apply to every client. --allow-net=api.example.com:443 limits fetch, database drivers and other net-based clients to that port, and a typo’d port matches nothing. Also, localhost and 127.0.0.1 are different grants.

  • Env vars read as undefined without --allow-env, so process.env.PORT || 3000 uses 3000. The denial summary lists the variable (read environment variable PORT). 3va start --port 4000 grants PORT automatically. Other variables need an explicit grant.

  • Express 5 printed noise to stderr in 3va 2.9.0: one [ERROR] [3va-requireFrom] node: spec=node:http … line for every require("node:*"). It was a leftover debug log and is gone in 2.12.0.

  • Production: 3va start server.js --name api supervises and restarts the process, like pm2 (Process Manager).