---
title: "Bolt Middleware"
description: "Document `createHandler(app, receiver)` and where `LogLevel` is set for both app and receiver. Add a small Bolt middleware that reads `event_id` and `ts/thread_ts` from the payload and exposes them as `context.correlation`. Listeners log from `context`. Propose a tiny logger wrapper that pulls correlation fields automatically."
canonical_url: "https://vercel.com/academy/slack-agents/bolt-nitro-middleware-and-logging"
md_url: "https://vercel.com/academy/slack-agents/bolt-nitro-middleware-and-logging.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-09T09:43:06.816Z"
content_type: "lesson"
course: "slack-agents"
course_title: "Slack Agents on Vercel with the AI SDK"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Bolt Middleware

# Bolt Middleware: Add Cross-Cutting Concerns Once, Use Everywhere

You need to add auth checks to 15 listeners. Or log every request. Or track rate limits. Copy-pasting the same code into every handler is fragile—miss one and you have a gap. Bolt middleware lets you write it once and it runs for every event automatically. This lesson teaches the pattern using correlation logging as the example, but you'll use middleware for auth, metrics, feature flags, and more throughout the course.

## Outcome

Understand how to use Bolt middleware to add cross-cutting concerns (logging, auth, metrics) that run automatically for every event, demonstrated through correlation tracking.

## Fast Track

1. Create `server/listeners/types.ts` with correlation type definitions
2. Add correlation middleware to `server/app.ts` before listeners
3. Update `app-mention.ts` to use structured logging with correlation fields
4. Test with bot mention and verify `event_id` appears in logs

\*\*Warning: Prerequisites\*\*

**You MUST complete [Lesson 1.3 (Boot Checks & Health)](./boot-checks-and-health) before starting this lesson.**

That lesson created `server/env.ts` with a Zod-validated `env` object that replaces `process.env`. All code in this lesson uses `env.SLACK_BOT_TOKEN` and `env.NODE_ENV` instead of `process.env.*`. The code won't work without it.

## How Bolt Middleware Works

Middleware runs before your event listeners. Write it once, it applies to every event automatically:

```
Slack Event → [Nitro] → [Receiver] → [Middleware] → [Your Listener]
                                        ↑
                                   Runs for EVERY event:
                                   - Add context (correlation, auth)
                                   - Log requests
                                   - Check permissions
                                   - Track metrics
```

**This lesson:** Use middleware to add correlation fields to `context`.\
**Later lessons:** Use the same pattern for auth checks, rate limiting, and feature flags.

The pattern is the point—correlation is just the teaching example.

\*\*Note: VercelReceiver, Not Socket Mode\*\*

This course uses `VercelReceiver` to connect Bolt to Nitro’s HTTP handler instead of Socket Mode. Socket Mode keeps a WebSocket open from your bot to Slack (great for on-prem or dev tunnels), while `VercelReceiver` turns Slack’s HTTP events into Bolt requests that run on Vercel/Nitro. That’s why you see `receiver = new VercelReceiver(...)` in `server/app.ts` and HTTP event subscriptions in your manifest—not `app.start()` or Socket Mode flags.

\*\*Note: Structured Logging with Pino\*\*

Bolt uses [pino](https://github.com/pinojs/pino) for logging. The signature is `logger.info(object, message)` not `logger.info(message)`. The object comes first so log aggregators (Datadog, CloudWatch, etc.) can index these fields for queries like `event_id='Ev123'`. This is different from `console.log` where you'd just log a string.

**Example:**

```typescript
// ❌ Old style (not queryable)
logger.info(`Processing event ${event_id} from user ${user}`);

// ✅ New style (queryable by event_id or user)
logger.info({ event_id, user }, "Processing event");
```

## Hands-On Exercise 2.2

Create middleware that adds context to all events for better debugging:

**Requirements:**

1. Create correlation middleware in `server/app.ts` that extracts event identifiers
2. Add `event_id`, `ts`, and `thread_ts` to `context.correlation`
3. Update at least one listener to use correlation fields in logging
4. Test that you can trace a single event through all log layers

**Implementation hints:**

- Use `app.use()` before `registerListeners()` to ensure middleware runs first
- Extract fields from `body` and `payload` - they have different structures
- Spread `context.correlation` into logger calls for structured logging
- Test both success and error paths to verify correlation persists

**Key files:**

- `/slack-agent/server/app.ts` - Middleware registration
- `/slack-agent/server/listeners/events/app-mention.ts` - Example listener to update

## Try It

1. **Start the app and mention your bot**:
   - Run `slack run`
   - In Slack, mention your bot: `@yourbot hello`
   - Watch terminal for correlated logs

2. **Verify correlation fields appear**:
   You should see something like this (exact format varies by terminal):

   ```
   [INFO]  bolt-app {
     event_id: 'Ev09E5EDA89M',
     ts: '1757523346.437659',
     thread_ts: undefined,
     channel: 'C09D4DG727P',
     user: 'U09D6B53WP4'
   } Processing app_mention
   ```

   **What to look for:** `event_id` and `ts` are present in the log. The exact formatting (spacing, colors, brackets) depends on your terminal and logger configuration. `thread_ts` will be `undefined` for top-level mentions and only has a value when someone replies in a thread.

3. **Test error correlation** (optional):
   - Temporarily throw an error in your listener
   - Verify error logs also include correlation fields

## Commit

```bash
git add -A
git commit -m "feat(logging): add correlation middleware for event tracing

- Add middleware to extract event_id, ts, thread_ts
- Attach correlation context to all events
- Update app-mention listener with structured logging
- Verify correlation fields appear in logs"
```

## Done-When

- [x] Created `server/listeners/types.ts` with context type augmentation
- [x] Created correlation middleware in `server/app.ts`
- [x] Middleware extracts `event_id`, `ts`, and `thread_ts` into `context.correlation`
- [x] Updated `app-mention.ts` to log correlation fields in three places (initial log + both error handlers)
- [x] Verified you can trace a single event through logs using `event_id`
- [x] (Optional) Applied correlation logging to other listeners for full coverage

\*\*Note: Extend to All Listeners\*\*

For production-ready correlation, apply this same logging pattern to your other listeners: slash commands (`server/listeners/commands/`), shortcuts, direct messages, etc. The middleware already provides `context.correlation` everywhere—you just need to spread it into your log calls.

## Step by Step Solution

### Step 1: Add correlation middleware and TypeScript types

Create a type definition file that extends Bolt's context and defines payload types:

```typescript title="/server/listeners/types.ts"
import "@slack/bolt";

// Slack event payload types (not fully exposed by Bolt's TypeScript definitions)
export type SlackEventBody = { 
  event_id?: string;
};

export type SlackEventPayload = { 
  ts?: string;
  message_ts?: string;
  thread_ts?: string;
  item?: { ts?: string };
};

// Extend Bolt's Context type to include our correlation fields
// This is TypeScript module augmentation - we're extending Bolt's existing Context interface
// without modifying their code. Now context.correlation will autocomplete in your IDE.
declare module "@slack/bolt" {
  interface Context {
    correlation?: {
      event_id?: string;
      ts?: string;
      thread_ts?: string;
    };
  }
}
```

\*\*Note: Why Type Casting?\*\*

The fields (`event_id`, `ts`, etc.) ARE present at runtime—Slack's API guarantees them. Bolt validates and parses these payloads but doesn't expose them in TypeScript types. This is a limitation of Bolt's type definitions, not your code. We define minimal types with just the fields we need, then cast once. This documents expectations without `any` casts everywhere.

Then add the middleware to `server/app.ts` after creating the app but before registering listeners:

```typescript title="/server/app.ts"
import { App, LogLevel } from "@slack/bolt";
import { VercelReceiver } from "@vercel/slack-bolt";
import registerListeners from "./listeners";
import { env } from "./env";
import { SlackEventBody, SlackEventPayload } from "./listeners/types";

const logLevel =
  env.NODE_ENV === "development" ? LogLevel.DEBUG : LogLevel.INFO;

const receiver = new VercelReceiver({
  logLevel,
});

const app = new App({
  token: env.SLACK_BOT_TOKEN,
  signingSecret: env.SLACK_SIGNING_SECRET,
  receiver,
  deferInitialization: true,
  logLevel,
});

// Add correlation middleware (runs after Bolt parses the event but before routing to listeners)
app.use(async ({ context, body, payload, next }) => {
  // Cast to our defined types (Bolt validates these, but doesn't expose them in types)
  const b = body as SlackEventBody;
  const p = payload as SlackEventPayload;
  
  // Slack uses different timestamp field names depending on event type:
  // - Regular events (app_mention, etc.): payload.ts
  // - Message events: payload.message_ts
  // - Reaction events: payload.item.ts
  // We check all three to handle any event type
  context.correlation = {
    event_id: b.event_id,
    ts: p.ts ?? p.message_ts ?? p.item?.ts,
    thread_ts: p.thread_ts,
  };
  
  // Pass control to the next middleware or listener
  // Without this, the request pipeline stops here and your listeners won't run
  await next();
});

registerListeners(app);

export { app, receiver };
```

### Step 2: Update a listener to use correlation

Add correlation logging to `server/listeners/events/app-mention.ts` in three locations:

**Edit 1: Replace the existing debug log** (at the start of the function, after the parameter destructuring):

```typescript
const appMentionCallback = async ({
  event,
  say,
  client,
  logger,
  context,
}: AllMiddlewareArgs & SlackEventMiddlewareArgs<"app_mention">) => {
  // Replace this:
  logger.debug(`app_mention event received: ${JSON.stringify(event)}`);
  
  // With this:
  logger.info({
    ...context.correlation,
    channel: event.channel,
    user: event.user,
  }, "Processing app_mention");
  
  const thread_ts = event.thread_ts || event.ts;
  const channel = event.channel;
  // ... rest of the function
```

**Edit 2: Update the main error handler** (in the first `catch` block):

```typescript
  try {
    // ... AI streaming logic ...
    
    await streamer.stop({
      blocks: [feedbackBlock({ thread_ts })],
    });
  } catch (error) {
    // Replace this:
    logger.error("app_mention handler failed:", error);
    
    // With this:
    logger.error({
      ...context.correlation,
      error: error instanceof Error ? error.message : String(error),
    }, "app_mention handler failed");
    
    try {
      await say({
        text: "Sorry, something went wrong...",
        thread_ts: event.thread_ts || event.ts,
      });
    // ... rest of error handling
```

**Edit 3: Update the fallback error handler** (in the nested `catch` block):

```typescript
  } catch (error) {
    logger.error({
      ...context.correlation,
      error: error instanceof Error ? error.message : String(error),
    }, "app_mention handler failed");
    try {
      await say({
        text: "Sorry, something went wrong processing your message. Please try again.",
        thread_ts: event.thread_ts || event.ts,
      });
    } catch (error) {
      // Replace this:
      logger.error("Failed to send error response:", error);
      
      // With this:
      logger.error({
        ...context.correlation,
        error: error instanceof Error ? error.message : String(error),
      }, "Failed to send error response");
    }
  }
};
```

### Step 3: Test correlation

1. Run `slack run`
2. Mention your bot in a channel
3. Look for correlated logs like this real example:

```
[INFO]  bolt-app {
  event_id: 'Ev09E5EDA89M',
  ts: '1757523346.437659',
  thread_ts: undefined,
  channel: 'C09D4DG727P',
  user: 'U09D6B53WP4'
} Processing app_mention
```

### Step 4: Test the error path (optional)

To see error correlation in action, temporarily break something:

```typescript title="/slack-agent/server/listeners/events/app-mention.ts"
// Add this line right after the try { to force an error
throw new Error("Testing correlation in error logs");
```

You'll see:

```
[ERROR] bolt-app {
  event_id: 'Ev09E5EDA89M',
  ts: '1757523346.437659',
  thread_ts: undefined,
  error: 'Testing correlation in error logs'
} app_mention handler failed
```

The same `event_id` and `ts` appear in both success and error logs, making it trivial to trace what went wrong for a specific user interaction.

## Troubleshooting

**Context.correlation is undefined:**

- Check middleware is added before `registerListeners(app)`
- Verify `app.use()` is called after creating the app

**Correlation fields missing from logs:**

- Ensure you're spreading `...context.correlation` in logger calls
- Check the middleware is actually running (add a console.log to verify)


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
