buddy
.chapterjs
node_modules
public
src
presence.ts
.env
.env.example
.gitignore
package.json
tsconfig.json
The one rule: what a file exports says what it is
Everything your bot does lives insrc/. ChapterJS never asks you to register anything: it loads every file of src/, and what the files export, made with one of its functions, is what the bot runs. The template has one file, src/presence.ts: what Discord shows under the name of your bot, “Getting ready”. Add a second one, a command, in src/commands/ping.ts:
src/commands/ping.ts
↻ Reloaded in 12 ms, 1 command, a presence loaded, then ↻ Commands updated on My Server, and /ping is in your test server. Nothing else says “this is a command”: command() does. Its name is what people type after the /, and the file only says what the command does. Change name to 'hello', and the command becomes /hello; move the file to src/fun/ping.ts, and nothing changes.
That is the rule for everything the framework runs:
A file may declare several of them, and a file that declares none is plain code:
src/lib/, src/db/, anything you import from the others. The templates of create chapterjs do both: your project keeps a folder per kind (src/commands/, and later src/events/), and tickets puts everything about tickets in src/tickets/: the command, the form, the buttons, and their texts in both languages. A folder is only a folder.
Why this design? Because the alternative, a list somewhere that says “the /ping command is in this file, the welcome event is in that one”, is a list you have to keep in sync with your files by hand. Here, there is no list. Adding a command is adding a declaration; removing one is deleting it.
The same import everywhere
Every file starts withimport { command } from 'chapterjs', or event, button… All of them come from the same package, chapterjs. There is nothing else to import: no client, no configuration, no file of the framework. And each function knows what to expect from what you give it: event({ name: 'memberJoin' }) gives your function a member, event({ name: 'messageCreate' }) a message, with the same import line.
.chapterjs/: generated, ignored, safe to delete
This folder is written by the framework, and git ignores it. It holds what your project adds to the types of chapterjs: the files of public/ that asset() offers, your languages, which type t, and your commands. Your editor reads it to complete what you type and to underline a mistake. It also keeps a small cache (what was last registered with Discord, so that starting again costs nothing) and, later, the compiled bot.
It is written when you install the project (postinstall runs chapterjs sync) and every time chapterjs dev starts. If your editor ever says that it does not know the files of public/ or your texts, run chapterjs sync (pnpm chapterjs sync, or the equivalent) and reload the editor. You can also delete the folder: it is rebuilt.
Try the editor
Opensrc/commands/ping.ts in your editor, and in run, type interaction. with the dot. The editor lists everything an interaction can do: reply, defer, edit, followUp, delete… Now type interaction.replu('Pong!') with a typo: it is underlined before you even save, with the method it does not know.
This is what “no type to write” means. Your files are TypeScript, so that the editor can do this, but every type comes from the framework. You never write one.
If nothing is underlined and nothing is completed, your editor is not reading
the types. In Visual Studio Code, make sure the folder you opened is the
project itself (the one with
package.json), not a folder above it, and run
chapterjs sync once.Try the hot reload
Start the bot (pnpm dev) and leave it running. Change the answer in src/commands/ping.ts:
src/commands/ping.ts
/ping again in Discord: the new answer. You never restart the bot while you write it: every file of src/ is watched, and a save reloads it, along with every file that imports it.
Now break the file on purpose: delete 'Pong! 🏓') so that the line reads await interaction.reply(;, and save.
/ping still answers with the last version that worked. Fix the file, save, and it reloads. A mistake never takes the bot down, so you can try things freely.
The other files
chapterjs is the only dependency of your bot. typescript and @types/node are development dependencies: they check your code in the editor and before a build, and are not needed to run the bot.
Your files, your order
As a project grows,src/commands/ fills up. Since the framework reads what files export and not where they are, sort them your way:
src
fun
dice.ts
ping.ts
moderation
shared.ts
warn.ts
- A folder is only a folder.
src/fun/dice.tsdeclares/diceas well assrc/commands/dice.tswould. Discord never sees your folders. - A file that declares nothing is yours.
shared.tsholds code several files share; import it like any file. - A file may declare several things.
warn.tscan hold the/warncommand, the button it shows and the event that logs it, each exported under its own name.
name, not of folder: name: 'mod warn' is /mod warn, wherever the file is. That is used later for commands like /mod ban.
Recap
- What a file exports says what it is:
command({ name: 'ping' })is/ping, and nothing registers it. - Every file imports its functions from
chapterjs, and each one knows what to expect from what you give it. .chapterjs/holds the generated types; your editor reads them;chapterjs syncrewrites them.- Save a file and the bot uses it; a broken file keeps its last working version.
- Folders and file names are yours to organise with: the framework reads what files export.
Your first command
Write /dice from scratch, and see what the framework checks for you.
Guide: Project structure
Every file and folder, in short.