Skip to main content

npm

Source

Description

A powerful HTTP framework for building your Discord bots, powered by node:http, @discordjs/rest, and @sapphire/pieces.

Features

  • Support for reloading and unloading commands
  • Built-in Hot Module Reloading for every store
  • Built-in logger, extendable by plugins
  • Support for attachment responses
  • Seamless integration with low-level libraries
  • Thin wrapper on top of raw data for maximum performance

Usage

This library can handle both HTTP interactions and registering commands both globally and per guild using an integrated design powered by decorators.

Command

The Command is a piece that runs for all chat input and context menu interactions, including auto-complete (since this one is sort of part of the former). Registering the commands happens with decorators:
You can also register subcommands via decorators:
Instead of a hand-written Args, you can let stars codegen type the options from the builder. Set codegen: { commands: true } in stars.config, run stars build, then stars codegen (see Typed command options). The CLI writes a CommandOptionsRegistry entry per command path, and Command.OptionsOf reads it. A renamed option, a flipped required, or a changed type then stops compiling:

Registering commands without decorators

If you don’t want to rely on TS decorators (for example, when writing plain JavaScript, or @sapphire/framework-style codebases), you can instead override the registerApplicationCommands method, which receives a per-command registry with the same capabilities as the decorators above:
Subcommands, subcommand groups, context menu commands, and guild restriction are all available on the registry:
Note: this is an alternative to the decorators, not a replacement — both approaches share the same underlying registry and can be mixed across different commands in the same project.

Utility decorators

ApplyOptions, the Requires* preconditions, Enumerable/EnumerableMethod, and the decorator factories (createClassDecorator, createMethodDecorator, createProxy, createFunctionPrecondition) live in @wolfstar/decorators since @wolfstar/http-framework 6.0.0. The framework only keeps the Register* decorators and RestrictGuildIds, which are part of command registration.
See the migration guide to move existing imports.

Client

The Client class contains the HTTP server, powered by node:http, it also registers a handler that processes whether or not the HTTP request comes from Discord and processes the information accordingly, handling the heavyweight in the background.

Logger

The framework ships a minimal logger, available as container.logger (and as client.logger) as soon as @wolfstar/http-framework is imported. It writes to the matching console method and filters entries by LogLevel, which defaults to LogLevel.Info:
The built-in implementation is intentionally bare: it has no timestamps, colours, or transports. Those belong to a logger plugin, which replaces it by assigning an ILogger to options.logger.instance from a preGenericsInitialization hook:
Because the plugin only has to satisfy the ILogger interface, the rest of the framework, including Hot Module Reloading and the command router, keeps logging through container.logger without any change.

Plugins

A plugin hooks into the Client lifecycle. Since 6.1.0, you define one as a plain object with definePlugin, the way Vite plugins are written. definePlugin is an identity helper: it only gives the object, or a factory that returns it, its types.
Every hook receives the Client and the credential-free ClientOptions the client keeps, so a hook can rewrite what a later hook sees. An apply function is called once per client and plugin, with the options, and its answer holds for every hook of that plugin. Register a plugin for every client with Client.use, or for one client with the plugins option. Both accept nested arrays and drop falsy entries, so you can enable a plugin conditionally:
When a hook throws or rejects, the framework wraps the failure in a PluginHookError. Its pluginName and hook properties name the plugin and the hook that failed, and cause holds the original error.
The legacy class-based API (Plugin subclasses with symbol hooks such as preGenericsInitialization, Client.use(PluginClass), and registerXHook) is deprecated. It still works through an adapter, but each legacy plugin emits a one-time DeprecationWarning with the code HTTP_FRAMEWORK_LEGACY_PLUGIN. Port it to definePlugin.
To ship a plugin as an installable package that projects activate from stars.config, wrap it in a module with @wolfstar/kit.

Client events

The Client extends an event emitter typed by the ClientEvents interface. Every event name is also available as a member of the Events enum, which is the recommended way to reference them, as the plain strings remain valid:
The Hot Module Reloading events are listed in their own section.

Expected errors: *Denied events

A UserError (or a subclass such as PreconditionError and ArgumentError) is an expected failure that you show to the user. When a command, an autocomplete handler, or an interaction handler throws one, the client emits it on a *Denied event instead of the matching *Error event: Any other error still goes to the *Error events. A listener that reports bugs, such as the Sentry listener of @wolfstar/shared-http-pieces, no longer receives the expected failures. The error event and the HTTP response behave as for any other thrown error.
To apply the same check in your own code, use the exported isUserError(error) helper. It accepts instanceof UserError. It also accepts any Error with a string identifier and a context property, because a bundle that inlines the framework has its own UserError class.
Before 6.3.0, a thrown UserError was emitted as commandError, autocompleteError, or interactionHandlerError. If a listener on those events handles UserError or PreconditionError, move it to the matching *Denied event.
Listeners declared as pieces can use the enum too:

Preconditions

A precondition is a check that runs before a chat input or context menu command, before the autocomplete of a command, and before an interaction handler. Use preconditions for checks shared across pieces, such as owner-only commands or role requirements. When a precondition denies the interaction, the framework skips the method and emits the error on chatInputCommandDenied, contextMenuCommandDenied, autocompleteDenied, or interactionHandlerDenied. These are the same events that receive a thrown UserError.

Write a precondition

A precondition is a piece. Create a class that extends Precondition in the preconditions directory, and the client loads it into the preconditions store. Implement chatInputRun, contextMenuRun, autocompleteRun, and/or interactionHandlerRun. Return this.ok() to accept, or this.error(...) to deny with a PreconditionError for the piece. Extend AllFlowsPrecondition instead to require both chatInputRun and contextMenuRun.
Each method receives the interaction, the command (or interaction handler), and the context of the entry that requested it.

Apply preconditions to a command

List the preconditions a command needs in the preconditions option of Command.Options. InteractionHandler.Options has the same option. Each entry is one of:
  • The name of a precondition piece.
  • An object with the name and a context, which the piece receives as its last argument.
  • A PreconditionFunction, for a check that does not need a piece of its own.
Entries run in the order listed, and the first denial stops the rest.

Combine preconditions with AND and OR

An entry can also be an array. The top-level list is an AND, so every entry has to pass. An array nested in it is an OR, so one entry has to pass. When none passes, the last error is the denial. An array nested in an OR is an AND again, and so on. The following list means Connect && (Moderator || (DJ && SongAuthor)):
By default, entries run one after the other and stop as soon as the result is known. To start every entry at once, write the list as { entries, mode: PreconditionRunMode.Parallel }. Nested arrays inherit the mode. In parallel mode, every entry runs. The denial is the first error in order for an AND and the last one for an OR.

Global preconditions

A precondition with a position is global. It runs for every command and interaction handler, in ascending order of position, before the preconditions of the piece.

Missing methods and errors

  • A piece with no method for the chat input or context menu command it checks denies it with Identifiers.PreconditionMissingChatInputHandler or Identifiers.PreconditionMissingContextMenuHandler.
  • For an autocomplete or an interaction handler, a piece with no autocompleteRun or interactionHandlerRun is skipped, global ones included. A check written for commands therefore does not deny their autocomplete or your buttons. PreconditionFunction entries of a command do not run for its autocomplete.
  • A skipped entry is left out of the result of its array, so it cannot make an OR pass.
  • A name that is not in the store denies with Identifiers.PreconditionUnavailable.
  • A precondition that throws is handled like a command that throws. A UserError goes to the *Denied events, and anything else goes to commandError.

Precondition events

A command emits these events in order:
  1. preChatInputCommandRun (or preContextMenuCommandRun).
  2. The preconditions run.
  3. chatInputCommandAccepted (or contextMenuCommandAccepted) and commandRun, or the *Denied event.
  4. commandSuccess or commandError, then commandFinish.
An autocomplete emits autocompleteAccepted right before autocompleteRun. An interaction handler emits interactionHandlerAccepted right before interactionHandlerRun. A denied interaction does not emit commandRun, autocompleteRun, or interactionHandlerRun. Without preconditions, the events are unchanged. Preconditions follow the design of @sapphire/framework, without the message flow, the built-in checks (Cooldown, NSFW, GuildOnly, …), and the typed registry of precondition names. The framework ships no built-in preconditions. The permission decorators of @wolfstar/decorators keep working alongside them.

Hot Module Reloading

@wolfstar/http-framework ships with Hot Module Reloading (HMR) as a core feature, no plugin required. When enabled, every path registered in every store is watched, and pieces are loaded, reloaded, and unloaded in place as their files are created, changed, and deleted, without restarting the process.
The hmr option accepts all of chokidar’s options, plus: The reloader is exposed as client.hmr, which is null when HMR is disabled, and can be stopped at any time:
It can also be used standalone, without a Client, as long as the stores are registered in the container:
The client emits an event for every operation, which is useful to react to changes, for example to push the updated application commands to Discord while developing:
Note: unloading a command also removes its entry from the ApplicationCommandRegistry, so a reloaded command is registered exactly once. HMR does not push the updated commands to Discord on its own, subscribe to the events above if you want that behaviour.

Project configuration (stars.config.*)

The typed project configuration consumed by the stars CLI — the defineConfig helper and the config loader — lives in @wolfstar/schema, a small package shared by @wolfstar/http-framework and @wolfstar/cli so neither depends on the other’s runtime. @wolfstar/http-framework re-exports its public surface unchanged as @wolfstar/http-framework/config, so any tool can resolve a project’s configuration without pulling in @wolfstar/cli. Most projects should keep importing it from here rather than depending on @wolfstar/schema directly. The package also exposes @wolfstar/http-framework/schema, forwarding the shared schema’s types and loader, like Nuxt’s nuxt/schema. Both facades preserve the same exports and function identities; existing /config imports remain supported.
Installing @wolfstar/http-framework also gives a project the stars binary. The framework depends on @wolfstar/cli and exposes it as stars, the way nuxt exposes nuxi’s binary, so a project needs no separate @wolfstar/cli install to run stars dev and stars build.
@wolfstar/http-framework/config has no side effects — importing it (or a stars.config.ts that imports it) never starts the bot. loadStarsConfig discovers stars.config.{ts,mts,cts,js,mjs,cjs} from a directory, applies defaults, validates every option and resolves all paths to absolute ones. modules lists installable modules, such as the @wolfstar/plugin-* packages, that the stars CLI installs while it loads the project. Each entry is a package specifier or a [specifier, options] tuple, and falsy entries are dropped:
See @wolfstar/kit for how modules work and how to write one. dev.url, the URL stars dev shows and health-checks the bot on, needs no configuration either: it is detected the way Vite’s and Nuxt’s dev servers are, from HTTP_PORT (env var, src/.env*/.env*, or dev.env) or 3000, and localhost is swapped for 127.0.0.1 at runtime if that is what is actually reachable. Set dev.url explicitly only to override it, e.g. for a LAN address: dev: { url: 'http://192.168.1.5:3000' }. dev.banner replaces the default Stars wordmark in the interactive CLI. Use a string or an array of lines, for example dev: { banner: ['★ STARYL', 'Twitch notifications'] }, or false to hide it. Up to four lines fit the compact panel (dev.layout: 'panel'). The dashboard, which a terminal of at least 90×20 gets, shows the project’s name instead. Application logs and standalone banners go to the log stream, not over the dev UI. dev also carries optional overrides for the dev loop. Press t in the TUI to toggle a quick tunnel without any configuration:
With the hmr option of the Client enabled, a change that only touches pieces is left to the bot. stars dev does not restart it, and the hmr channel of the dev UI shows what was reloaded. The bot itself feeds the channels that report what happens inside it (hmr, commands, interactions, http, lifecycle). See stars dev. tunnel.updateEndpoint writes the public URL to the Discord application’s interactions_endpoint_url; it is opt-in because it edits a live application, and needs DISCORD_TOKEN in the environment or the project’s .env. tunnel.provider: 'ngrok' opens the tunnel through ngrok instead of cloudflared. See Tunnel providers.

The build (tsdown)

tsdown is the bundler stars build and stars dev use, and it is configured from stars.config itself. A base project configures nothing at all — this is a complete build:
The defaults are the configuration a bot would otherwise write out by hand:

Aliases

The four prefixes Nuxt gives every project work out of the box, pointing at the same two places its own do:
The build resolves them on its own; TypeScript needs the matching paths (the scaffold writes them, and examples/basic shows them):
A project’s own tsdown.alias is added to these rather than replacing them, and a target written as a relative path ('./src/lib') is resolved against the project root, the way every other path in stars.config is — a module id ('preact/compat') is left alone. So the block is for what the defaults cannot know — an extra alias, declaration output, or a target. Conventional src/locales assets are copied to dist/locales automatically by the CLI:
Anything in tsdown wins over the defaults, and plugins are appended rather than replaced. With future.compatibilityVersion: 3 (legacy mode, end-of-life) a tsdown.config.* in the project root is still loaded and tsdown is merged over it, so a project can move its options across one at a time. From 4 on the block is the whole configuration, and a leftover tsdown.config.* is reported instead of being silently ignored. vite: {} works the same way for build.tool: 'vite' (see experimental flags): it is merged into the project’s own vite.config.*, the way vite: {} in a Nuxt config is.

Compatibility version

future.compatibilityVersion selects the build-default generation. Version 6 is the default, and versions 5 and 4 remain supported. Version 7 is an opt-in preview. Version 3 is end-of-life. It still works for projects that have a standalone tsdown.config.*, but every command prints a COMPATIBILITY_VERSION_EOL warning, and the next major removes it.
From version 4 on:
  • Auto imports are on with the tsdown build tool, and the autoImports() plugin is wired into the build by stars itself instead of by the project’s own configuration file. The framework’s exports and the project’s src/lib/**, src/utils/** are usable without an import statement, the same way Nuxt’s own are.
  • tsdown is configured from stars.config alone. A tsdown.config.* (or a package.json#tsdown field) raises TSDOWN_CONFIG_FILE_UNSUPPORTED, because a build that quietly dropped the plugins such a file declares would be far harder to diagnose than an error naming it.
  • build.tool: 'auto' resolves to tsdown for any TypeScript entry, rather than looking for a tsdown.config.* or a tsdown dependency first. tsc stays available as an explicit choice.
Version 5 adds one thing: stars registers the environment for you. It calls setup() from @wolfstar/env-utilities before any module of the bot runs, so the bot no longer loads its .env* files by hand. Version 6 splits the generated TypeScript configuration. stars prepare writes .stars/tsconfig.app.json for the bot sources and .stars/tsconfig.node.json for root files such as stars.config.*, instead of a single .stars/tsconfig.json. The project’s own tsconfig.json becomes a solution-style file that references both. A tsdown build defaults build.tsconfig to the app config. Pin 5 or 4 to keep the single .stars/tsconfig.json. See generated TypeScript configuration. stars info prints the version in effect, and any warning about it.

Environment (env)

env mirrors the options of setup() from @wolfstar/env-utilities. stars writes them into the built entry and calls setup() with them as the entry’s very first import. This runs before @wolfstar/plugin-* registrations and the bot’s own modules, all of which may read process.env as soon as they load.
  • env: false turns the registration off. env: true turns it on with the defaults. Setting any option also opts in, which requires @wolfstar/env-utilities in the project’s dependencies (ENV_REQUIRES_ENV_UTILITIES).
  • enabled: false turns the registration off while keeping the options, for example for stars dev’s port lookup.
  • Without an env block, registration is on from compatibility version 5 when the project depends on @wolfstar/env-utilities. It stays off with experimental.enableNitro, because serverless presets have no .env files to load.
  • Only serializable options are mirrored, since they end up in the build output as JSON. path stays relative and @wolfstar/env-utilities resolves it against the bot’s working directory at runtime, so the output does not depend on the machine that built it.
  • prefix keeps only the variables starting with it (for example BOT_). debug logs every file loaded and every prefix match.
  • tsdown, Vite and Nitro builds register it through the entry transform. build.tool: 'tsc' and 'none' never pass through it. stars dev preloads it with node --import, but in production such a bot loads its environment itself (or runs with node --import @wolfstar/env-utilities/setup). The projects @wolfstar/create-http-framework generates for them set env: false and keep calling setup() by hand.
  • stars dev reads HTTP_PORT for the default dev.url from the same path/env files, after the env:options hook runs.

Hooks

hooks registers lifecycle hooks of the stars CLI, run with hookable the way Nuxt’s own hooks are. Use them to react to builds, adjust the env or tsdown options at build time, or run scripts around stars prepare and stars dev. Key each hook by its full name or nest it under its namespace, as a function or an array of functions:
dev:restart receives 'initial', 'build', 'manual', or 'crash' as its reason. Hooks run in the CLI process, never in the bot. Use env:options to change what the bot receives. An unknown hook name fails with UNKNOWN_HOOK rather than registering a hook that never runs. In stars dev, a hook that throws is logged and the watcher keeps going. In stars build, it fails the build. stars build and stars dev run them in the same order: config:resolved → env:options → builder:created → prepare:before → prepare:done, then build:before → build:done for each build. stars dev adds dev:start once it is watching, dev:restart before each (re)start of the bot, and dev:close on shutdown. stars build awaits every hook. In stars dev, build:before does not delay the rebuild it reports, while build:done, dev:restart, and dev:close run one after another. A slow build:done settles before the restart it triggered, and before shutdown.

Experimental flags

experimental is the same kind of block Nuxt’s own experimental is: opt-in booleans, all false by default, each guarding work that is still landing.
The resolved configuration is also available programmatically:
Invalid options raise a ConfigError with a stable code, the offending option path, the file it came from, and an actionable hint. See the @wolfstar/cli README for the full option reference and how the stars commands (dev, build, info, codegen, prepare, commands) use it. See Configuration Errors for every stars.config.* diagnostic code.

ApplicationCommandRegistry

The ApplicationCommandRegistry is @wolfstar/http-framework’s centralized registry and uses @discordjs/rest to register them in Discord.

Sync commands on startup

Command registration is off by default. Set commands.register to 'on-start' in the Client options to synchronize the loaded commands with Discord every time the bot starts:
client.listen() runs the synchronization after the HTTP server listens and every plugin’s postListen hook has finished. The registry first fetches the deployed commands and compares them with the loaded ones. It then writes only the differences: it creates missing commands with POST, updates changed commands with PATCH, and deletes commands with DELETE only when removeUnknown is enabled. When nothing differs, the client logs Commands: N up to date. Otherwise it logs the number of created, updated, and removed commands. If a Discord API call fails, the client logs a warning and keeps listening. The bot serves interactions with the commands that are already deployed. The client skips startup registration when STARS_COMMANDS_DUMP=1 is set, which is how stars commands reads the registry without touching Discord. commands accepts these options: Unknown deployed commands are kept by default. As a result, removing or renaming a local command leaves its previous definition deployed. Enable removeUnknown only when this bot owns every command in the selected scopes. Without guildIds, global commands go to the global scope and guild-restricted commands go to their guilds. With guildIds, only those guilds are synchronized. Global-only fields such as contexts and integration_types are left out of guild writes. The registry does not discover guilds that no longer have restricted commands. Pass their IDs in guildIds, or clean them with stars commands clean.
The framework takes no distributed lock. In sharder or broker deployments, and across separate replicas, enable registration on one process only with when:

Sync commands manually

Bots that serve interactions through client.fetch(), such as Nitro deployments, never call listen(). Call client.registry.sync() yourself after loading the pieces. It accepts the same options as commands, except register:
sync() resolves with the counts of created, updated, removed, and unchanged commands. With onConflict: 'log', the counts describe the proposed changes. Concurrent sync() calls in the same process share the running operation and its options. sync() attempts every scope even when one fails. After it logs the partial result, it rejects with an AggregateError that holds one error per failed scope. Each error names the scope (global scope or guild <id>) and keeps the original failure as its cause. Catch it to retry or to fail startup:
To read what Discord has deployed, call client.registry.fetchDeployed(guildId?). It returns the global commands, or the commands of the given guild, including their full localization dictionaries.

Push commands in bulk

To overwrite whole scopes with Discord’s bulk endpoints instead, keep automatic registration off and push after loading the commands:
However, if you want to use the registry without the client, you can do so:
Note: calling applicationCommandRegistry.setup() is not needed if you are using the Client class because it is already called automatically for you.