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.
Basic middleware
Section titled “Basic middleware”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`);});Injecting data with pop.locals
Section titled “Injecting data with pop.locals”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:
import "popii-framework";
declare module "popii-framework" { interface PopiiLocals { dbUser: { id: string; balance: number; premium: boolean }; }}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.
Stopping the pipeline
Section titled “Stopping the pipeline”Return without calling next() to short-circuit. The command handler never runs:
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();});Error-handling middleware
Section titled “Error-handling middleware”A middleware that accepts three parameters — (err, pop, next) — is an error handler. Popii detects it by function.length, exactly like Express:
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});Per-command middleware
Section titled “Per-command middleware”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 },});Execution order
Section titled “Execution order”- Global middlewares from
src/middlewares/(alphabetical) - Per-command middlewares (in array order)
- The command’s
dohandler - Error middlewares — only if something above threw
Built-in middleware from plugins
Section titled “Built-in middleware from plugins”Several plugins inject middleware automatically:
| Plugin | What 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 |