Middleware
Intercept and control command execution
Middleware acts as a checkpoint that every command and event passes through before it executes. Use it for logging, access control, or payload modification.
Conceptual flow diagram showing middleware execution: user interaction passes through '01-logger', '02-auth', '03-cooldown' middleware before reaching the command handler, with an abort path from auth
For middleware config fields (order, enabled), execution order behavior, error handling, and the enabled vs disabled distinction, see the @robojs/discordjs Middleware reference.
Creating Middleware
Create files in src/middleware/. Middleware runs in alphabetical order by filename. Use number prefixes to control execution order.
import type { MiddlewareData } from '@robojs/discordjs'
export default (data: MiddlewareData) => {
console.log(`Executing: ${data.record.key}`)
}export default (data) => {
console.log(`Executing: ${data.record.key}`)
}MiddlewareData
The data parameter passed to each middleware function contains information about the handler being intercepted and its arguments.
| Field | Type | Description |
|---|---|---|
record.key | string | Handler identifier (e.g., commands/ping) |
record.type | string | Handler type (commands, events, context) |
record.metadata | object | Handler metadata |
payload | unknown[] | Arguments passed to the handler (first element is usually the interaction) |
Modifying Payloads
Return a MiddlewareResult with a modified payload array to change the arguments passed to the handler.
import type { MiddlewareData, MiddlewareResult } from '@robojs/discordjs'
export default (data: MiddlewareData): MiddlewareResult => {
// Add a timestamp to the payload
data.payload.push(Date.now())
return { payload: data.payload }
}export default (data) => {
// Add a timestamp to the payload
data.payload.push(Date.now())
return { payload: data.payload }
}Aborting Execution
Return { abort: true } to prevent the handler from running. This is useful for access control or conditional logic.
import type { MiddlewareData, MiddlewareResult } from '@robojs/discordjs'
import type { ChatInputCommandInteraction } from 'discord.js'
export default (data: MiddlewareData): MiddlewareResult => {
const interaction = data.payload[0] as ChatInputCommandInteraction
const member = interaction.member
if (!member) {
return { abort: true }
}
}export default (data) => {
const interaction = data.payload[0]
const member = interaction.member
if (!member) {
return { abort: true }
}
}Error Handling
Errors thrown in middleware halt execution of the associated handler. Implement proper error handling to avoid unintended disruptions.
Real-World Patterns
Command Cooldown
Prevent users from spamming commands by tracking the last execution time per user.
import type { MiddlewareData, MiddlewareResult } from '@robojs/discordjs'
import type { ChatInputCommandInteraction } from 'discord.js'
const cooldowns = new Map<string, number>()
const COOLDOWN_MS = 5000
export default (data: MiddlewareData): MiddlewareResult => {
if (data.record.type !== 'commands') return
const interaction = data.payload[0] as ChatInputCommandInteraction
const key = `${interaction.user.id}:${data.record.key}`
const lastUsed = cooldowns.get(key) ?? 0
const remaining = COOLDOWN_MS - (Date.now() - lastUsed)
if (remaining > 0) {
interaction.reply({
content: `Please wait ${Math.ceil(remaining / 1000)}s before using this again.`,
ephemeral: true
})
return { abort: true }
}
cooldowns.set(key, Date.now())
}const cooldowns = new Map()
const COOLDOWN_MS = 5000
export default (data) => {
if (data.record.type !== 'commands') return
const interaction = data.payload[0]
const key = `${interaction.user.id}:${data.record.key}`
const lastUsed = cooldowns.get(key) ?? 0
const remaining = COOLDOWN_MS - (Date.now() - lastUsed)
if (remaining > 0) {
interaction.reply({
content: `Please wait ${Math.ceil(remaining / 1000)}s before using this again.`,
ephemeral: true
})
return { abort: true }
}
cooldowns.set(key, Date.now())
}Discord showing a user attempting to run a command too quickly, receiving an ephemeral 'Please wait 3s before using this again.' message from cooldown middleware
Logging with Timing
Measure how long each command takes by logging before and after execution.
import { logger } from 'robo.js/logger.js'
import type { MiddlewareData, MiddlewareResult } from '@robojs/discordjs'
export default (data: MiddlewareData): MiddlewareResult => {
const start = Date.now()
logger.info(`[start] ${data.record.key}`)
// Attach timing data to the payload for the handler to access if needed
data.payload.push({ __middlewareStart: start })
return { payload: data.payload }
}import { logger } from 'robo.js/logger.js'
export default (data) => {
const start = Date.now()
logger.info(`[start] ${data.record.key}`)
// Attach timing data to the payload for the handler to access if needed
data.payload.push({ __middlewareStart: start })
return { payload: data.payload }
}Since middleware only runs before the handler, end-to-end timing must be measured within the handler itself. Read the attached __middlewareStart timestamp from the payload and log the elapsed time at the end of your handler logic.
Feature Flag
Check a feature flag before allowing a command to run. This is useful for rolling out new commands gradually or disabling commands without removing files.
import type { MiddlewareData, MiddlewareResult } from '@robojs/discordjs'
import type { ChatInputCommandInteraction } from 'discord.js'
const disabledCommands = new Set(['experimental', 'beta-feature'])
export default (data: MiddlewareData): MiddlewareResult => {
if (data.record.type !== 'commands') return
const commandName = data.record.key.replace('commands/', '')
if (disabledCommands.has(commandName)) {
const interaction = data.payload[0] as ChatInputCommandInteraction
interaction.reply({ content: 'This command is currently disabled.', ephemeral: true })
return { abort: true }
}
}const disabledCommands = new Set(['experimental', 'beta-feature'])
export default (data) => {
if (data.record.type !== 'commands') return
const commandName = data.record.key.replace('commands/', '')
if (disabledCommands.has(commandName)) {
const interaction = data.payload[0]
interaction.reply({ content: 'This command is currently disabled.', ephemeral: true })
return { abort: true }
}
}