Middleware Basics

Learn how to use middleware to add cross-cutting concerns to your bot.

Middleware functions run before your command handlers and event listeners. They're perfect for authentication, logging, rate limiting, and other cross-cutting concerns that apply to multiple commands.


What Is Middleware?

Middleware is a function that runs in the processing pipeline before your command handlers. It receives the bot context and a next function to continue the chain. Think of middleware as a series of filters or processing steps that each request must pass through before reaching your command handlers.

The middleware pattern allows you to separate cross-cutting concerns (like authentication, logging, rate limiting) from your business logic. Instead of adding this code to every command handler, you write it once as middleware and apply it to all requests. This makes your code more maintainable and easier to test.

type Middleware = (
  ctx: BotContext,
  next: () => Promise<void>
) => Promise<void>

Creating Middleware in Separate Files

The Bot.middleware() static method helps you create middleware with validation and type safety. This is especially useful when organizing middleware in separate files—it ensures your middleware follows the correct signature and catches errors early.

Using Bot.middleware() provides several benefits:

  • Type safety: TypeScript will enforce the correct middleware signature
  • Runtime validation: Catches invalid middleware at creation time, not runtime
  • Better organization: Keep middleware in separate files for better maintainability
  • IDE support: Better autocomplete and error detection

Here's how to create middleware in a separate file:

// src/bot/middleware/auth.ts
import { Bot } from '@igniter-js/bot'

export const authMiddleware = Bot.middleware(async (ctx, next) => {
  if (!isAuthorized(ctx.message.author.id)) {
    await ctx.bot.send({
      provider: ctx.provider,
      channel: ctx.channel.id,
      content: {
        type: 'text',
        content: '❌ Unauthorized'
      }
    })
    return // Block request
  }
  await next() // Continue to next middleware/handler
})

Then import and use it in your bot configuration:

// src/bot.ts
import { Bot, telegram } from '@igniter-js/bot'
import { authMiddleware } from './middleware/auth'

const bot = Bot.create({
  adapters: {
    telegram: telegram({ /* ... */ })
  },
  middlewares: [authMiddleware],
  commands: {
    // ...
  }
})

The Bot.middleware() method validates that:

  • The middleware is a function
  • The function accepts exactly 2 parameters (ctx and next)

If validation fails, you'll get a clear error message at middleware creation time, making debugging much easier.


Basic Middleware

Here's a simple logging middleware created using Bot.middleware():

// src/bot/middleware/logging.ts
import { Bot } from '@igniter-js/bot'

export const loggingMiddleware = Bot.middleware(async (ctx, next) => {
  console.log(`[${ctx.event}] Message from ${ctx.message.author.name}`)
  await next() // Continue to next middleware/handler
})

Use it in your bot:

import { Bot, telegram } from '@igniter-js/bot'
import { loggingMiddleware } from './middleware/logging'

const bot = Bot.create({
  adapters: {
    telegram: telegram({ /* ... */ })
  },
  middlewares: [loggingMiddleware],
  commands: {
    // ...
  }
})

Calling next()

Always call await next() unless you want to stop processing. The next() function passes control to the next middleware in the chain (or the command handler if no more middleware exists). Calling next() is how middleware "continues" the request pipeline—without it, processing stops and the command handler never runs.

Understanding when to call next() and when to skip it is crucial for middleware development. Most middleware should call next() to allow the request to continue, but authentication middleware might skip it to block unauthorized users. Here are examples of both patterns:

// ✅ Good - calls next()
const middleware: Middleware = async (ctx, next) => {
  console.log('Before')
  await next() // Continue processing
  console.log('After')
}

// ✅ Good - blocks processing intentionally
const authMiddleware: Middleware = async (ctx, next) => {
  if (!isAuthorized(ctx.message.author.id)) {
    await ctx.bot.send({
      provider: ctx.provider,
      channel: ctx.channel.id,
      content: {
        type: 'text',
        content: '❌ Unauthorized'
      }
    })
    return // Don't call next() - block processing
  }
  await next() // Authorized - continue
}

// ❌ Bad - forgets to call next()
const badMiddleware: Middleware = async (ctx, next) => {
  console.log('Processing')
  // Missing await next() - processing stops here!
}

Middleware Execution Order

Middleware runs in the order it's registered in your middlewares array. When you create middleware using Bot.middleware(), they're validated immediately, ensuring they're properly structured before registration. This validation catches errors early and provides clear error messages if something is wrong.

The execution order matters because middleware can modify the context or block requests. For example, you want authentication middleware to run before expensive operations, and logging middleware typically runs first to capture all requests. Understanding execution order helps you compose middleware effectively.

// src/bot/middleware/first.ts
import { Bot } from '@igniter-js/bot'

export const middleware1 = Bot.middleware(async (ctx, next) => {
  console.log('1. Before')
  await next()
  console.log('1. After')
})

// src/bot/middleware/second.ts
import { Bot } from '@igniter-js/bot'

export const middleware2 = Bot.middleware(async (ctx, next) => {
  console.log('2. Before')
  await next()
  console.log('2. After')
})

// src/bot.ts
import { middleware1 } from './middleware/first'
import { middleware2 } from './middleware/second'

const bot = Bot.create({
  middlewares: [middleware1, middleware2],
  // ...
})

// Execution order:
// 1. Before
// 2. Before
// Command handler runs
// 2. After
// 1. After

Common Use Cases

Here are common middleware patterns you'll use in production bots. Each example uses Bot.middleware() for validation and type safety. Understanding these patterns helps you build robust, production-ready bots with proper authentication, logging, rate limiting, and more:


Dynamic Middleware Registration

Add middleware at runtime using bot.use(). This method allows you to register middleware after your bot has been created, which is useful for conditional middleware loading, plugin systems, or runtime configuration. Dynamic registration follows the same execution order rules as static middleware—it's added to the end of the middleware chain.

const bot = Bot.create({
  // ...
})

// Add middleware dynamically
bot.use(async (ctx, next) => {
  console.log('Dynamic middleware')
  await next()
})

Error Handling

Handle errors in middleware to prevent crashes and provide graceful error recovery. Middleware can catch errors from downstream handlers (command handlers or other middleware) and handle them appropriately—logging errors, sending user-friendly messages, or re-throwing for further processing. This pattern ensures your bot remains stable even when unexpected errors occur.

const errorHandlingMiddleware: Middleware = async (ctx, next) => {
  try {
    await next()
  } catch (error) {
    // Log error
    console.error('Middleware error:', error)
    
    // Send error message to user
    await ctx.bot.send({
      provider: ctx.provider,
      channel: ctx.channel.id,
      content: {
        type: 'text',
        content: '❌ An error occurred. Please try again later.'
      }
    })
    
    // Re-throw to let bot's error handler catch it
    throw error
  }
}

Best Practices

Following these practices ensures your middleware integrates smoothly with the bot core and performs reliably. Good middleware is predictable, efficient, and resilient to errors. It enhances your bot's functionality without introducing complexity or bugs.


Complete Example

Here's a complete example that brings together multiple middleware functions to demonstrate how they work together in a production bot. This example shows the recommended middleware order and how each middleware contributes to the overall bot functionality. You can use this as a template for your own bots, adapting the middleware to your specific needs.

This example demonstrates:

  • Middleware Composition: How multiple middleware functions work together
  • Execution Order: The importance of middleware ordering
  • Separation of Concerns: Each middleware handles a specific cross-cutting concern
  • Production Patterns: Real-world patterns you'll use in production bots

Example with Bot.middleware():

// src/bot.ts
import { Bot, telegram } from '@igniter-js/bot'
import { authMiddleware } from './middleware/auth'
import { loggingMiddleware } from './middleware/logging'
import { rateLimitMiddleware } from './middleware/rate-limit'

const bot = Bot.create({
  id: 'example-bot',
  name: 'Example Bot',
  adapters: {
    telegram: telegram({
      token: process.env.TELEGRAM_TOKEN!,
      handle: '@example_bot',
      webhook: {
        url: process.env.TELEGRAM_WEBHOOK_URL!
      }
    })
  },
  middlewares: [
    loggingMiddleware,    // Logs first
    rateLimitMiddleware,  // Then rate limit
    authMiddleware        // Then authenticate
  ],
  commands: {
    // Commands here...
  }
})

Execution Order:

  1. loggingMiddleware logs the request
  2. rateLimitMiddleware checks rate limits
  3. authMiddleware verifies authorization
  4. Command handler executes (if authorized and within limits)
  5. Middleware cleanup runs in reverse order