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

Source: https://docs.agentbeam.com/cookbooks/frameworks/express

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](https://docs.agentbeam.com/cookbooks/app-monitoring#step-1-turn-it-on-and-create-the-apps-token) 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

```sh
npm install @agent-beam/beam
```

```ts
// beam.ts
import { createAppMonitor } from "@agent-beam/beam/monitor";

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

## Report each request after the response

Express:

```ts
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:

```ts
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.

To also refuse senders the workspace [paused](https://docs.agentbeam.com/agent-cli/app-monitoring#pausing-attackers) (scanners, password guessers, LLM abusers), use the built-in middleware instead of your own. It answers a paused sender with a 429 and reports every other request after its response, with the status:

```ts
app.set("trust proxy", 1);
app.use(beam.middleware()); // first, before your routes
```

With Fastify, check in an `onRequest` hook: `const v = beam.check({ ip: request.ip, url: request.url, headers: request.headers }); if (v.paused) return reply.code(429).header("Retry-After", String(v.retryAfter)).send();`. Set `trust proxy` (or Fastify's `trustProxy`) to the number of proxies in front of the app, so `req.ip` is the last address your proxy added. Beam won't pause an address the client could have written into `X-Forwarded-For`.

## Report sign-ins, admins and keys

```ts
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

```ts
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

```sh
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

```yaml
# .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](https://docs.agentbeam.com/agent-cli/app-monitoring#levels-and-detection) and the [attack-in-progress signals](https://docs.agentbeam.com/agent-cli/app-monitoring#signs-an-attack-is-underway):

| 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 `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

| 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](https://docs.agentbeam.com/cookbooks/app-monitoring#step-8-send-alerts-to-the-right-place)) |
| Pausing | `beam.middleware()` first, and **Settings > Pausing** on after a dry run |
| Routine | Someone reads **Needs action** daily and **Attack analytics** weekly |
