Express and Fastify
Monitor an Express or Fastify API with the in-process SDK, reporting each request after the response so every finding knows whether the attack worked.
On this page
Node.js servers use the SDK. Because Express and Fastify can report after the response, every finding carries its status: Beam raises the attacks your app answered and rolls up the ones it refused.
What you'll have
- Every request checked with its status, time and client.
- Sign-ins, admin grants, new keys and LLM spend reported.
- New outbound hosts noticed.
- Dependencies checked on every pull request.
Before you start
Do steps 1 and 2 of the general cookbook first: App monitoring is on, the app is added, and its beam_app_ token is in your secret store as BEAM_APP_TOKEN. Everything below needs @agent-beam/beam 0.2.32 or newer.
Install and create one monitor
npm install @agent-beam/beam
// beam.ts
import { createAppMonitor } from "@agent-beam/beam/monitor";
export const beam = createAppMonitor({ stack: "express", watchOutbound: true });
Report each request after the response
Express:
import express from "express";
import { beam } from "./beam";
const app = express();
app.set("trust proxy", 1); // behind one proxy or load balancer, so req.ip is the client
app.use((req, res, next) => {
const started = Date.now();
res.on("finish", () => {
beam.request({
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get("user-agent"),
status: res.statusCode,
durationMs: Date.now() - started,
headers: req.headers,
});
});
next();
});
Fastify:
import Fastify from "fastify";
import { beam } from "./beam";
const app = Fastify({ trustProxy: true });
app.addHook("onResponse", async (request, reply) => {
beam.request({
method: request.method,
url: request.url,
ip: request.ip,
userAgent: request.headers["user-agent"],
status: reply.statusCode,
durationMs: Math.round(reply.elapsedTime),
headers: request.headers,
});
});
Register these first, so they see requests your other middleware rejects. A long-running server flushes every 5 seconds on its own.
Report sign-ins, admins and keys
app.post("/login", async (req, res) => {
const user = await verifyPassword(req.body);
if (!user) {
beam.event("login.failed", { user: await userIdFor(req.body.username), ip: req.ip });
return res.status(401).json({ error: "Invalid credentials" });
}
beam.event("login.succeeded", { user: user.id, ip: req.ip });
// ...start the session
});
// Where roles or keys change
beam.event("admin.granted", { user: target.id, detail: `by ${req.user.id}` });
beam.event("apikey.created", { user: req.user.id });
With Passport, call the same events in your strategy's verify callback.
Protect LLM endpoints
app.post("/chat", async (req, res) => {
const check = beam.prompt(req.body.message, { user: req.user.id });
if (check.severity === "high" || check.severity === "critical") return res.status(400).json({ error: "Refused" });
const result = await callModel(req.body.message);
beam.usage({ user: req.user.id, tokens: result.usage.total_tokens, model: result.model });
res.json({ text: beam.output(result.text).flagged ? escapeHtml(result.text) : result.text });
});
Prove it works
beam monitor status # app, level, stack, usage
beam monitor test # a test finding through your alert workflows
curl -s "https://your-app.example.com/?id=1%20UNION%20SELECT%201" -o /dev/null
Within a minute, the SQL injection test shows on App monitoring: under Needs action if the app answered 2xx, or counted under "needed nothing from you" if it refused it. Then sign in to a test account with a wrong password ten times and the right one once: Sign-in succeeded after 10 failed attempts appears as critical.
Check dependencies on every pull request
# .github/workflows/beam-cve.yml
name: Dependency check
on: [pull_request]
jobs:
cve:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx -y @agent-beam/beam cve check . --fail-on high
env:
BEAM_APP_TOKEN: ${{ secrets.BEAM_APP_TOKEN }}
What Beam watches for on this stack
Besides every built-in rule and the attack-in-progress signals:
| Rule | Means | Severity |
|---|---|---|
| Node.js package files served | .npmrc, package-lock.json or node_modules answered 2xx | High |
| Prototype pollution | __proto__ or constructor.prototype in a request | Medium |
| GraphQL introspection | A __schema query in a GET, if you serve GraphQL | Medium |
Attacks on other frameworks (WordPress probes, Laravel or Spring exploits, and so on) are rolled up as Not your stack when there's no status to say otherwise, and never alert.
Close what Beam would catch
Beam raises these when they're found; better that there's nothing to find:
- Never serve the project root as static files; serve only a
publicdirectory. - Set
trust proxyto the exact number of proxies in front of you, soreq.ipcan't be spoofed. - Use
helmet()and a body size limit.
Coverage checklist
| Feature | Done when |
|---|---|
| SDK after the response | Findings show Answered or Refused |
| Stack declared | stack: "express" |
| Sign-in events | login.failed and login.succeeded in the login route |
| Admin and key events | admin.granted and apikey.created where they happen |
| LLM endpoints | prompt(), output() and usage() on every model call |
| Outbound hosts | Hosts your apps call lists your providers |
| Dependency checks | The CI job runs on pull requests |
| Alerts | Critical pages someone, high reaches a channel (step 8) |
| Routine | Someone reads Needs action daily and Attack analytics weekly |
Set this up with your coding agent
Install the agentbeam-setup skill for Claude Code, Codex, Cursor, Gemini CLI, OpenCode and Copilot, then ask your agent to set up AgentBeam on this machine. Beam scans the skill before it installs.
npx -y @agent-beam/beam add skill agentbeam-setup -g --all-agents