npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

discord-embed-router

v1.2.1

Published

An Express-inspired router for discord.js embeds and interactions

Readme

discord-embed-router

npm version license

An Express-inspired, URL-path-based router for discord.js interactions. Define routes once, render an embed for each, and get buttons/select menus/modals that "link" to them for free: no more hand-rolled customId parsing.

Why

Handling Discord message components usually means switching on customId strings by hand, threading state through them yourself, and hoping two features never pick the same prefix. discord-embed-router treats your bot's UI like a tiny web app instead:

  • Routes: register a path (/catalog/:id) and a handler that returns the embed/components to render, the same way you'd write an Express route and its response.
  • Component builders: RouteButtonBuilder, RouteStringSelectMenuBuilder, RouteModalBuilder, etc. encode the destination path/query into the component's customId for you.
  • Sessions: attach per-message state that persists across clicks without you managing a cache.
  • Cleanup/timeout: declare what a stale component should turn into once its window expires.

Install

npm install discord-embed-router discord.js

discord.js is a peer dependency: you bring your own compatible version (see peerDependencies in package.json for the supported range).

Quick start

import { EmbedRouter, RouteButtonBuilder } from "discord-embed-router";
import {
	ActionRowBuilder,
	ButtonStyle,
	Client,
	EmbedBuilder,
} from "discord.js";

const client = new Client({ intents: [] });
const router = new EmbedRouter<undefined, number>(client);
router.onError(console.error);

router.get("/counter", (embedRouter, interaction, state) => {
	const value = state.session.get() ?? 0;
	state.session.set(value);

	return {
		embeds: [new EmbedBuilder().setTitle("Counter").setDescription(`${value}`)],
		components: [
			new ActionRowBuilder()
				.addComponents(
					new RouteButtonBuilder(embedRouter)
						.setLabel("+1")
						.setStyle(ButtonStyle.Success)
						.setTo("/counter/increment", { method: "POST" }),
				)
				.toJSON(),
		],
		// required whenever a route touches the session, so the router knows
		// when to drop the stored count
		timeout: 5 * 60 * 1000,
	};
});

router.post("/counter/increment", (_embedRouter, _interaction, state) => {
	state.session.set((state.session.get() ?? 0) + 1);
	return { redirect: "/counter" };
});

client.on("interactionCreate", async (interaction) => {
	if (
		interaction.isChatInputCommand() &&
		interaction.commandName === "counter"
	) {
		await router.dispatch(interaction, "/counter");
	}
});

client.login(process.env.DISCORD_TOKEN);

Clicking "+1" fires a POST /counter/increment, which mutates the count stored in the message's session and redirects to GET /counter to re-render. Storing the count in a session rather than baking it into the button's customId matters here: the router serializes dispatches per message, so a read-modify-write against the session can't race itself the way two rapid clicks would if each button's customId captured a stale value at render time (both clicks would send the same "value + 1" and one increment would be lost).

See examples/basic-bot for a full bot with multiple routes, a catalog page, and command wiring.

Core concepts

Routes

router.get/post/put/patch/delete(path, handler) registers a handler for a path, using path-to-regexp syntax for params (/user/:id). router.modal(path, handler) registers a handler that returns a modal to show instead of editing the message. A GET handler's return value is the new message content; other methods must return a redirect (to a registered GET path) or undefined (silent ack).

When one path implements several methods, router.route(path, handlers) registers them all at once from an object keyed by lowercase method, which is handy when a module already exports its handlers that way:

router.route("/counter", { get, post, modal } satisfies RouteHandlers<...>);

Routers can be nested with router.use(prefix, subRouter), mirroring how you'd compose an Express app.

Component builders

RouteButtonBuilder, RouteStringSelectMenuBuilder, RouteChannelSelectMenuBuilder, RouteRoleSelectMenuBuilder, RouteUserSelectMenuBuilder, and RouteModalBuilder extend their discord.js counterparts, replacing setCustomId/setURL with .setTo(path, { method, query }). The router encodes the path into a compact customId and decodes it back on click.

Sessions

Pass a Session type parameter to EmbedRouter<Globals, Session, Locals> and use state.session.get()/set()/delete() inside a handler to keep state tied to a message across multiple interactions (e.g. a multi-step form). Any handler that sets a session or a cleanup must also return a timeout, so the router always knows when to give up on stale state.

Cleanup and timeouts

A GET handler can return { cleanup, timeout } alongside its content. If no further interaction lands on that message before timeout ms, cleanup runs and its return value (if any) is applied to the message, which is handy for expiring a form or disabling stale buttons.

Encoding

By default, paths are compacted with HashEncoder so customIds stay under Discord's 100-character limit even for deeply nested routes. Pass a custom Encoder via the EmbedRouter constructor if you need different encoding behavior.

API reference

Full type-level documentation is available via your editor's hover/autocomplete (all exports are documented with TSDoc). Public exports:

  • EmbedRouter
  • RouteButtonBuilder, RouteModalBuilder, RouteStringSelectMenuBuilder, RouteStringSelectMenuOptionBuilder, RouteChannelSelectMenuBuilder, RouteRoleSelectMenuBuilder, RouteUserSelectMenuBuilder
  • Encoder, HashEncoder
  • ConfigError

Changelog

Notable changes per release are documented in the changelog.

License

MIT