npm
Source
Description
A powerful HTTP framework for building your Discord bots, powered bynode: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: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:
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.
Client
TheClient 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 ascontainer.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:
ILogger to options.logger.instance from a
preGenericsInitialization hook:
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 theClient 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:
PluginHookError. Its pluginName and hook
properties name the plugin and the hook that failed, and cause holds the original error.
To ship a plugin as an installable package that projects activate from stars.config, wrap it in a module with
@wolfstar/kit.
Client events
TheClient 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.
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.
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 onchatInputCommandDenied, 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 extendsPrecondition 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.
context of the entry that
requested it.
Apply preconditions to a command
List the preconditions a command needs in thepreconditions 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
nameand acontext, which the piece receives as its last argument. - A
PreconditionFunction, for a check that does not need a piece of its own.
Combine preconditions with AND and OR
An entry can also be an array. The top-level list is anAND, 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)):
{ 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 aposition 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.PreconditionMissingChatInputHandlerorIdentifiers.PreconditionMissingContextMenuHandler. - For an autocomplete or an interaction handler, a piece with no
autocompleteRunorinteractionHandlerRunis skipped, global ones included. A check written for commands therefore does not deny their autocomplete or your buttons.PreconditionFunctionentries 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
ORpass. - A name that is not in the store denies with
Identifiers.PreconditionUnavailable. - A precondition that throws is handled like a command that throws. A
UserErrorgoes to the*Deniedevents, and anything else goes tocommandError.
Precondition events
A command emits these events in order:preChatInputCommandRun(orpreContextMenuCommandRun).- The preconditions run.
chatInputCommandAccepted(orcontextMenuCommandAccepted) andcommandRun, or the*Deniedevent.commandSuccessorcommandError, thencommandFinish.
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.
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:
Client, as long as the stores are registered in the container:
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.
@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:
@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:
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:
Aliases
The four prefixes Nuxt gives every project work out of the box, pointing at the same two places its own do:paths (the scaffold writes them, and
examples/basic shows them):
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:
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
tsdownbuild tool, and theautoImports()plugin is wired into the build bystarsitself instead of by the project’s own configuration file. The framework’s exports and the project’ssrc/lib/**,src/utils/**are usable without animportstatement, the same way Nuxt’s own are. tsdownis configured fromstars.configalone. Atsdown.config.*(or apackage.json#tsdownfield) raisesTSDOWN_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 totsdownfor any TypeScript entry, rather than looking for atsdown.config.*or atsdowndependency first.tscstays available as an explicit choice.
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: falseturns the registration off.env: trueturns it on with the defaults. Setting any option also opts in, which requires@wolfstar/env-utilitiesin the project’s dependencies (ENV_REQUIRES_ENV_UTILITIES).enabled: falseturns the registration off while keeping the options, for example forstars dev’s port lookup.- Without an
envblock, registration is on from compatibility version 5 when the project depends on@wolfstar/env-utilities. It stays off withexperimental.enableNitro, because serverless presets have no.envfiles to load. - Only serializable options are mirrored, since they end up in the build output as JSON.
pathstays relative and@wolfstar/env-utilitiesresolves it against the bot’s working directory at runtime, so the output does not depend on the machine that built it. prefixkeeps only the variables starting with it (for exampleBOT_).debuglogs 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 devpreloads it withnode --import, but in production such a bot loads its environment itself (or runs withnode --import @wolfstar/env-utilities/setup). The projects@wolfstar/create-http-frameworkgenerates for them setenv: falseand keep callingsetup()by hand. stars devreadsHTTP_PORTfor the defaultdev.urlfrom the samepath/envfiles, after theenv:optionshook 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.
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
TheApplicationCommandRegistry 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. Setcommands.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.
Sync commands manually
Bots that serve interactions throughclient.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:
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:Note: callingapplicationCommandRegistry.setup()is not needed if you are using theClientclass because it is already called automatically for you.