Popii - v0.5.1
    Preparing search index...

    Middleware

    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!");
    }
    });
    1. Global middlewares (from src/middlewares/, alphabetical order)
    2. Per-command middlewares (in array order)
    3. The command's do handler
    4. Error middlewares (only triggered if something above throws)

    Several built-in plugins inject their own middleware automatically when added to the plugins array:

    • errorHandlerPlugin() — catches PopiiError and shows user-friendly messages
    • commandLoggerPlugin() — logs execution time and errors
    • permissionGuardPlugin() — enforces Discord permission requirements
    • commandAnalyticPlugin() — tracks usage counts per command