As33
@periodic/
arsenic

Express Integration

The expressContext middleware attaches request context to all queries made within Express route handlers. Uses AsyncLocalStorage for zero-overhead propagation.

Setup

typescript
import express from 'express';
import { createMonitor, expressContext } from '@periodic/arsenic';

const app = express();
const monitor = createMonitor({ /* config */ });

// MUST be before routes
app.use(expressContext(monitor, {
  attachUser: (req) => req.user?.id, // optional
}));

// All queries in handlers below will have request context
app.get('/api/users', async (req, res) => {
  const users = await User.find({ active: true });
  res.json(users);
});

app.listen(3000);

Add middleware BEFORE routes

expressContext must be added before any route handlers. Adding it after routes means those routes' queries will have no request context.

With user attribution

typescript
// If using JWT auth
app.use(expressContext(monitor, {
  attachUser: (req) => {
    // Return any string — user ID, email, etc.
    return req.user?.id || req.headers['x-user-id'] as string;
  },
}));

// Event output will include:
// { "request": { "userId": "user_abc123", ... } }

With multiple routers

typescript
import express from 'express';
import { Router } from 'express';

const app = express();
const apiRouter = Router();

// Attach context to app — covers all routers
app.use(expressContext(monitor));
app.use(express.json());

// Mount routers normally
app.use('/api', apiRouter);

apiRouter.get('/users', async (req, res) => {
  // Request context is available here
  const users = await User.find();
  res.json(users);
});

Event context field

json
{
  "request": {
    "id": "req_8f29a3b1c",
    "method": "GET",
    "route": "/api/users/:id",
    "userId": "user_abc123"
  }
}
Queries made outside of Express handlers (e.g. background jobs, startup scripts) will have no request field. This is expected behavior — Arsenic still monitors these queries and emits events, just without HTTP context.