LogoRobo.js

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

FocusThe linear middleware chain with numbered execution orderZoom100%NotesCreate a horizontal flow diagram with labeled boxes: 'User Interaction' → '01-logger' → '02-auth' → '03-cooldown' → 'Command Handler'. Draw a dashed arrow from '02-auth' downward to an 'Abort' label. Use a clean, minimal style with rounded rectangles. This is a conceptual diagram, not a screenshot of actual UI.

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.

01-logger.tsRuns first
02-auth.tsRuns second
src/middleware/01-logger.ts
import type { MiddlewareData } from '@robojs/discordjs'

export default (data: MiddlewareData) => {
	console.log(`Executing: ${data.record.key}`)
}
src/middleware/01-logger.js
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.

FieldTypeDescription
record.keystringHandler identifier (e.g., commands/ping)
record.typestringHandler type (commands, events, context)
record.metadataobjectHandler metadata
payloadunknown[]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.

src/middleware/add-timestamp.ts
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 }
}
src/middleware/add-timestamp.js
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.

src/middleware/require-member.ts
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 }
	}
}
src/middleware/require-member.js
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.

src/middleware/01-cooldown.ts
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())
}
src/middleware/01-cooldown.js
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

FocusThe ephemeral cooldown error messageZoom100%NotesShow a channel where a user has tried to run a command within the cooldown period. The bot displays an ephemeral cooldown message with the 'Only you can see this' label visible.

Logging with Timing

Measure how long each command takes by logging before and after execution.

src/middleware/01-timing.ts
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 }
}
src/middleware/01-timing.js
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.

src/middleware/02-feature-flag.ts
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 }
	}
}
src/middleware/02-feature-flag.js
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 }
	}
}

Next Steps

On this page