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

Code
npm install @agent-beam/beam
Code
// beam.ts
import { createAppMonitor } from "@agent-beam/beam/monitor";

export const beam = createAppMonitor({ stack: "express", watchOutbound: true });

Report each request after the response

Express:

Code
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:

Code
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

Code
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

Code
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

Code
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

Code
# .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:

RuleMeansSeverity
Node.js package files served.npmrc, package-lock.json or node_modules answered 2xxHigh
Prototype pollution__proto__ or constructor.prototype in a requestMedium
GraphQL introspectionA __schema query in a GET, if you serve GraphQLMedium

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 public directory.
  • Set trust proxy to the exact number of proxies in front of you, so req.ip can't be spoofed.
  • Use helmet() and a body size limit.

Coverage checklist

FeatureDone when
SDK after the responseFindings show Answered or Refused
Stack declaredstack: "express"
Sign-in eventslogin.failed and login.succeeded in the login route
Admin and key eventsadmin.granted and apikey.created where they happen
LLM endpointsprompt(), output() and usage() on every model call
Outbound hostsHosts your apps call lists your providers
Dependency checksThe CI job runs on pull requests
AlertsCritical pages someone, high reaches a channel (step 8)
RoutineSomeone 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
Explore the docs