Skip to main content
ImmutableLog logo
Back
ExpressNode.jsTypeScript

Express Integration

Integrate ImmutableLog into any Express application with a middleware that automatically captures all HTTP requests. Uses Node.js 18+ native `fetch` API for non-blocking sending.

Installation

No extra dependencies beyond Express. The integration uses Node.js 18+ native `fetch`. For older versions, install `node-fetch`.

bash
npm install express
# Node.js < 18 only:
npm install node-fetch

Middleware Code

Save the code below in `src/middleware/immutablelog.ts`. The middleware uses `res.on('finish')` to capture the final response status after the handler completes, and `res.on('error')` to capture stream errors.

typescript
// src/middleware/immutablelog.ts
import crypto from 'crypto';
import { Request, Response, NextFunction } from 'express';

const MAX_PAYLOAD_BYTES = 12_000;

function clampPayload(payload: object): string {
  let s = JSON.stringify(payload);
  if (Buffer.byteLength(s, 'utf8') <= MAX_PAYLOAD_BYTES) return s;
  const p = { ...(payload as Record<string, unknown>) };
  delete p.error;
  delete p.requestBody;
  s = JSON.stringify(p);
  if (Buffer.byteLength(s, 'utf8') <= MAX_PAYLOAD_BYTES) return s;
  return JSON.stringify({
    id: (payload as Record<string, unknown>).id,
    kind: (payload as Record<string, unknown>).kind,
    message: String((payload as Record<string, unknown>).message ?? 'event').slice(0, 500),
    timestamp: (payload as Record<string, unknown>).timestamp,
  });
}

function hashBody(body: string | Buffer): string {
  const buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
  return crypto.createHash('sha256').update(buf).digest('hex');
}

function severityFrom(status: number | null, err: Error | null): string {
  if (err) return 'error';
  if (!status) return 'info';
  if (status >= 400) return 'error';
  if (status >= 300) return 'info';
  if (status >= 200) return 'success';
  return 'info';
}

export interface ImmutableLogOptions {
  apiKey: string;
  apiUrl?: string;
  serviceName?: string;
  env?: string;
  skipPaths?: string[];
}

export function immutableLogMiddleware(opts: ImmutableLogOptions) {
  const {
    apiKey,
    apiUrl = 'https://api.immutablelog.com',
    serviceName = 'express-service',
    env = 'production',
    skipPaths = ['/health', '/healthz'],
  } = opts;
  const skip = new Set(skipPaths);

  return (req: Request, res: Response, next: NextFunction): void => {
    if (skip.has(req.path)) return next();

    const startedAt = Date.now();
    const requestId =
      (req.headers['x-request-id'] as string) || crypto.randomUUID();
    (req as Request & { requestId: string }).requestId = requestId;

    const rawBody: string =
      (req as Request & { rawBody?: string }).rawBody ?? '';

    const emit = (err?: Error): void => {
      const latencyMs = Date.now() - startedAt;
      const statusCode = err ? 500 : res.statusCode;
      const kind = severityFrom(statusCode, err ?? null);
      const eventName =
        (req as Request & { imtblEventName?: string }).imtblEventName ??
        `http.${req.method}.${(req.route?.path as string | undefined) ?? req.path}`;

      const payload: Record<string, unknown> = {
        id: crypto.randomUUID(),
        kind,
        message: err
          ? `${req.method} ${req.path} failed: ${err.constructor.name}`
          : kind === 'success'
          ? `${req.method} ${req.path} completed successfully`
          : `${req.method} ${req.path} processed`,
        timestamp: new Date().toISOString(),
        context: {
          ip: req.ip ?? req.socket?.remoteAddress ?? 'unknown',
          userAgent: req.headers['user-agent'] ?? 'unknown',
        },
        request: {
          requestId,
          method: req.method,
          path: req.path,
          queryParams: Object.keys(req.query).length ? req.query : null,
        },
        metrics: { latencyMs, statusCode },
        severity: kind === 'error' ? 'high' : 'low',
      };

      if (kind === 'error') {
        payload.error = {
          statusCode,
          retryable: [408, 429, 500, 502, 503, 504].includes(statusCode),
          ...(err && {
            exception: err.constructor.name,
            exceptionMessage: err.message,
          }),
          ...(rawBody && { requestBodyHash: hashBody(rawBody) }),
        };
      } else if (kind === 'success') {
        payload.success = {
          statusCode,
          result: statusCode === 200 ? 'ok' : 'processed',
        };
      } else {
        payload.info = { statusCode, action: req.method.toLowerCase() };
      }

      const event = {
        payload: clampPayload(payload),
        meta: {
          type: kind,
          event_name: eventName,
          service: serviceName,
          request_id: requestId,
          env,
        },
      };

      // fire-and-forget — nunca bloqueia a resposta / never blocks the response
      fetch(`${apiUrl}/v1/events`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': `${eventName}-${requestId}`,
          'Request-Id': requestId,
        },
        body: JSON.stringify(event),
        signal: AbortSignal.timeout(5000),
      }).catch((e: Error) =>
        console.warn('[ImmutableLog] emit failed:', e.message),
      );

      res.setHeader('x-request-id', requestId);
    };

    res.on('finish', () => emit());
    res.on('error', (err: Error) => emit(err));

    next();
  };
}

Registration and Configuration

Register the middleware globally with `app.use()` before your routes. Configure via environment variables to keep credentials out of the code.

typescript
// src/app.ts
import express from 'express';
import { immutableLogMiddleware } from './middleware/immutablelog';

const app = express();
app.use(express.json());

// Registrar antes das rotas / Register before routes
app.use(
  immutableLogMiddleware({
    apiKey: process.env.IMTBL_API_KEY!,
    apiUrl: process.env.IMTBL_URL ?? 'https://api.immutablelog.com',
    serviceName: process.env.IMTBL_SERVICE_NAME ?? 'express-service',
    env: process.env.IMTBL_ENV ?? 'production',
    skipPaths: ['/health', '/healthz', '/metrics'],
  }),
);

app.get('/users', (req, res) => res.json({ users: [] }));
app.listen(3000);

Never pass the token directly in code. Use environment variables (.env) and ensure the `.env` file is in `.gitignore`.

How it works

The middleware records the start timestamp on entry, listens to `res.on('finish')` to capture the final status and calculate latency. Sending to ImmutableLog is done via fire-and-forget `fetch` — it never blocks the response to the client.

entry

Timestamp recorded, request_id generated or inherited from header

res.on('finish')

Latency calculated, event emitted via fire-and-forget fetch

res.on('error')

Stream errors captured and emitted as error event

The x-request-id is injected into the response for end-to-end traceability across systems.

Custom event name

Assign `req.imtblEventName` in any handler or preceding middleware to override the automatically generated event name.

typescript
app.post('/checkout', (req, res) => {
  // Sobrescreve o nome do evento / Override the event name
  (req as any).imtblEventName = 'payment.checkout.initiated';

  // ... logica de negocio / business logic ...
  res.json({ status: 'ok' });
});

Auto default: http.METHOD.route_path (e.g., http.POST./checkout).

Path exclusion

Pass `skipPaths` in the options to ignore specific routes such as health checks and metrics.

typescript
app.use(
  immutableLogMiddleware({
    apiKey: process.env.IMTBL_API_KEY!,
    apiUrl: 'https://api.immutablelog.com',
    skipPaths: ['/health', '/healthz', '/metrics', '/readyz'],
  }),
);

Error enrichment

On error events (4xx/5xx), the middleware includes the SHA-256 hash of the request body, `retryable` flag, and when available, exception details.

typescript
// Error handler do Express — capturado automaticamente pelo middleware
// Express error handler — automatically captured by the middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  res.status(500).json({ error: err.message });
});

error.requestBodyHash

SHA-256 of body (audit without exposing data)

error.retryable

true for: 408, 429, 500, 502, 503, 504

error.exception

Error class name (if available)

error.exceptionMessage

Error message (if available)

This documentation reflects the current integration behavior. For questions or advanced integrations, contact the support team.