Middleware runs before (and optionally after) every command and snap handler. It's the right place for cross-cutting concerns: auth checks, logging, analytics, rate limiting, or injecting data into pop.locals.
Popii uses the same pipeline model as Express: each middleware receives (pop, next) and must call next() to continue. Global middlewares are loaded from src/middlewares/ alphabetically.
// src/middlewares/01-logger.ts
import { middleware } from "popii";
export default middleware(async (pop, next) => {
const start = Date.now();
await next();
pop.log.info(`/${(pop as any).interaction?.commandName} took ${Date.now() - start}ms`);
});
The 01- prefix ensures this runs first — middlewares are loaded in filename order.
pop.locals is a per-request scratch pad. Augment the type globally so TypeScript knows what's there:
// src/types.d.ts (or any .d.ts file in your project)
import "popii";
declare module "popii" {
interface PopiiLocals {
dbUser: { id: string; balance: number; premium: boolean };
}
}
// src/middlewares/02-load-user.ts
import { middleware } from "popii";
import { db } from "../db";
export default middleware(async (pop, next) => {
const row = db.query("SELECT * FROM users WHERE id = ?").get(pop.user.id);
pop.locals.dbUser = row ?? { id: pop.user.id, balance: 0, premium: false };
await next();
});
Now every command has pop.locals.dbUser fully typed.
Return without calling next() to halt execution:
// src/middlewares/03-maintenance.ts
import { middleware } from "popii";
export default middleware(async (pop, next) => {
if (process.env.MAINTENANCE_MODE === "true") {
await pop.reply({ content: "🔧 The bot is under maintenance. Try again later.", ephemeral: true });
return; // stop here — command handler never runs
}
await next();
});
A middleware with three parameters (err, pop, next) is an error handler. Popii detects it by the function's .length property, exactly like Express:
// src/middlewares/99-error-catch.ts
import { middleware } from "popii";
export default middleware(async (err, pop, next) => {
pop.log.error("Command error:", err);
await pop.reply({ content: "Something went wrong. Please try again.", ephemeral: true }).catch(() => {});
});
Always declare all three parameters explicitly. Default parameters or rest arguments reduce function.length and break detection.
Pass middlewares directly in a command definition to apply it only to that command:
import { command, middleware } from "popii";
const premiumOnly = middleware(async (pop, next) => {
if (!pop.locals.dbUser.premium) {
await pop.reply({ content: "This command is for Premium users only.", ephemeral: true });
return;
}
await next();
});
export default command({
name: "premium-feature",
description: "A premium-only command",
middlewares: [premiumOnly],
async do(pop) {
await pop.reply("Welcome, premium user!");
}
});
src/middlewares/, alphabetical order)do handlerSeveral built-in plugins inject their own middleware automatically when added to the plugins array:
errorHandlerPlugin() — catches PopiiError and shows user-friendly messagescommandLoggerPlugin() — logs execution time and errorspermissionGuardPlugin() — enforces Discord permission requirementscommandAnalyticPlugin() — tracks usage counts per command