Skip to content

Middleware

Middleware is code that runs in a pipeline before (and optionally after) every command and snap handler. Use it for anything that applies across many commands: authentication checks, loading database records, logging, rate limiting, or injecting shared data.

Popii’s middleware model is identical to Express: each middleware receives (pop, next) and must call next() to continue the chain. Files in src/middlewares/ are loaded in alphabetical order — prefix them with numbers to control ordering.

src/middlewares/01-logger.ts
import { middleware } from "popii-framework";
export default middleware(async (pop, next) => {
const start = Date.now();
await next();
const name = (pop as any).interaction?.commandName ?? "unknown";
pop.log.info(`/${name} completed in ${Date.now() - start}ms`);
});

pop.locals is the per-request scratchpad. Add data here in middleware and read it in any command or subsequent middleware. Augment the PopiiLocals interface to get TypeScript types everywhere:

src/types.d.ts
import "popii-framework";
declare module "popii-framework" {
interface PopiiLocals {
dbUser: { id: string; balance: number; premium: boolean };
}
}
src/middlewares/02-load-user.ts
import { middleware } from "popii-framework";
import { db } from "../db";
export default middleware(async (pop, next) => {
const row = db
.query("SELECT * FROM users WHERE id = ?")
.get(pop.user.id) as { id: string; balance: number; premium: boolean } | null;
pop.locals.dbUser = row ?? { id: pop.user.id, balance: 0, premium: false };
await next();
});

Every command downstream now has pop.locals.dbUser — typed, no boilerplate.

Return without calling next() to short-circuit. The command handler never runs:

src/middlewares/03-maintenance.ts
import { middleware } from "popii-framework";
export default middleware(async (pop, next) => {
if (process.env.MAINTENANCE === "true") {
await pop.reply({ content: "The bot is under maintenance.", ephemeral: true });
return; // command does not run
}
await next();
});

A middleware that accepts three parameters — (err, pop, next) — is an error handler. Popii detects it by function.length, exactly like Express:

src/middlewares/99-errors.ts
import { middleware } from "popii-framework";
export default middleware(async (err: Error, pop, next) => {
pop.log.error("Unhandled command error:", err);
await pop.reply({
content: "Something went wrong. Please try again.",
ephemeral: true,
}).catch(() => {}); // interaction may have already timed out
});

Apply middleware to a single command by passing it in the middlewares array:

import { command, middleware } from "popii-framework";
const premiumOnly = middleware(async (pop, next) => {
if (!pop.locals.dbUser.premium) {
await pop.reply({ content: "This command requires a Premium subscription.", ephemeral: true });
return;
}
await next();
});
export default command({
name: "export",
description: "Export your data",
middlewares: [premiumOnly],
async do(pop) {
// only premium users reach here
},
});
  1. Global middlewares from src/middlewares/ (alphabetical)
  2. Per-command middlewares (in array order)
  3. The command’s do handler
  4. Error middlewares — only if something above threw

Several plugins inject middleware automatically:

PluginWhat it does
errorHandlerPlugin()Catches PopiiError/PopiiSilentError and formats them as ephemeral replies
permissionGuardPlugin()Enforces permissions, allowedRoles, deniedRoles, allowedChannels
commandLoggerPlugin()Logs command name, user, guild, and duration
commandAnalyticPlugin()Increments in-memory usage counters per command