# Create a Discord bot (/guides/ecosystem/discordjs)

<!-- agent-signals: reading_time_min: 6 · est_tokens: 2462 · updated: 2026-07-28 -->
Related: [Build an app with Astro and Bun](/guides/ecosystem/astro.md), [Containerize a Bun application with Docker](/guides/ecosystem/docker.md), [Use Drizzle ORM with Bun](/guides/ecosystem/drizzle.md), [Use Gel with Bun](/guides/ecosystem/gel.md), [Build an HTTP server using Elysia and Bun](/guides/ecosystem/elysia.md), [Build an HTTP server using Express and Bun](/guides/ecosystem/express.md)

Discord.js runs on Bun with no extra setup. This guide builds a bot that answers a `/ping` slash command: you register the command once, then start the bot and use it in your server. If this is your first bot, copy each block as you reach it.

***

Create a folder for your bot and set it up with `bun init`. Pick the defaults when it asks.

```sh icon="terminal" title="terminal" terminal
mkdir my-bot
cd my-bot
bun init
```

***

Add Discord.js to the project.

```sh icon="terminal" title="terminal" terminal
bun add discord.js
```

***

Your bot needs its own account, which you create in Discord's developer portal. Open the [developer portal](https://discord.com/developers/applications), sign in, and create an **Application**. Inside it, open the **Bot** tab to create the bot user. Discord's [setup walkthrough](https://discordjs.guide/legacy/preparations/app-setup) has screenshots if you get lost.

Copy two values from the portal:

* The **token** on the **Bot** tab. It's the password your code uses to log in, so treat it like one and keep it to yourself.
* The **Application ID** on the **General Information** tab. Discord uses it to tie your commands to this app.

***

A bot can't do anything in a server until you invite it. In the portal's **OAuth2** section, generate an invite URL with the `bot` and `applications.commands` scopes, open it, and add the bot to a server you manage. Slash commands need the `applications.commands` scope, so don't skip it. A personal server you make for testing is the easiest place to start, and Discord's [guide to adding a bot](https://discordjs.guide/legacy/preparations/adding-your-app) walks through it with screenshots.

***

You also need your server's ID to register the command there. In Discord, turn on **Settings > Advanced > Developer Mode**, then right-click your server's icon and choose **Copy Server ID**.

***

Save all three values in `.env.local`. Bun reads this file on startup and loads it into `process.env`, so nothing secret lives in your code.

```ini icon="settings" title=".env.local"
DISCORD_TOKEN=your-bot-token
DISCORD_CLIENT_ID=your-application-id
DISCORD_GUILD_ID=your-server-id
```

***

Add `.env.local` to your `.gitignore` before you commit anything. Anyone who reads the token can control your bot, so it should never land in version control.

```txt icon="file-code" title=".gitignore"
node_modules
.env.local
```

***

Discord has to know about a command before anyone can use it. Register `/ping` with a short script named `deploy-commands.ts`.

```ts icon="/icons/typescript.svg" title="deploy-commands.ts"
import { REST, Routes, SlashCommandBuilder } from "discord.js";

const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_GUILD_ID } = process.env;
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID || !DISCORD_GUILD_ID) {
  throw new Error("Set DISCORD_TOKEN, DISCORD_CLIENT_ID, and DISCORD_GUILD_ID in .env.local");
}

// the commands you want to register
const commands = [new SlashCommandBuilder().setName("ping").setDescription("Replies with Pong!").toJSON()];

const rest = new REST().setToken(DISCORD_TOKEN);

// register them in your test server
await rest.put(Routes.applicationGuildCommands(DISCORD_CLIENT_ID, DISCORD_GUILD_ID), { body: commands });

console.log("Registered /ping");
```

Run it once.

```sh icon="terminal" title="terminal" terminal
bun run deploy-commands.ts
```

You only run this again when you add a command or change its name or description, not every time the bot starts. Registering to your server instead of globally keeps the command scoped to where you're testing.

***

Now the bot itself. Save it as `bot.ts`.

```ts icon="/icons/typescript.svg" title="bot.ts"
import { Client, Events, GatewayIntentBits } from "discord.js";

const { DISCORD_TOKEN } = process.env;
if (!DISCORD_TOKEN) {
  throw new Error("Set DISCORD_TOKEN in .env.local");
}

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

// runs once, right after the bot connects
client.once(Events.ClientReady, readyClient => {
  console.log(`Logged in as ${readyClient.user.tag}`);
});

// runs every time someone uses a slash command
client.on(Events.InteractionCreate, async interaction => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === "ping") {
    await interaction.reply("Pong!");
  }
});

client.login(DISCORD_TOKEN);
```

The ready handler logs a line once the bot connects. After that, `interactionCreate` runs whenever someone uses a slash command; it confirms the command was `/ping` and replies with `Pong!`.

***

Start the bot with `bun run`.

```sh icon="terminal" title="terminal" terminal
bun run bot.ts
```

The first connection takes a few seconds. Once the login line prints, switch to Discord and type `/ping` in your server.

```txt
Logged in as my-bot#1234
```

The bot replies with `Pong!`. You've got a working Discord bot.

***

To add another command, define it in `deploy-commands.ts`, run that script again, and add an `if` branch for its name in `bot.ts`. The [Discord.js docs](https://discord.js.org/docs) cover command options, permissions, buttons, embeds, and the rest of the API.

***

When you deploy, there's no build or bundling step. Bun runs `bot.ts` and every file it imports directly, so you ship your source as-is and start it with the same `bun run bot.ts` you use while developing.

`deploy-commands.ts` registers `/ping` in your test server, which is the right scope while you're building. To publish the bot to every server it joins, register globally instead: change the route to `Routes.applicationCommands(DISCORD_CLIENT_ID)`. Global registration doesn't use a server, so you can also drop `DISCORD_GUILD_ID` from the script's check and from `.env.local`.

To keep the bot online and bring it back after a crash or reboot, run it under a process manager.

<Columns cols="2">
  <Card title="systemd" href="/guides/ecosystem/systemd" icon="<svg xmlns=&#x22;http://www.w3.org/2000/svg&#x22; viewBox=&#x22;0 0 24 24&#x22; fill=&#x22;none&#x22;><path d=&#x22;M19 4H5C4.06812 4 3.60218 4 3.23463 4.15224C2.74458 4.35523 2.35523 4.74458 2.15224 5.23463C2 5.60218 2 6.06812 2 7C2 7.93188 2 8.39782 2.15224 8.76537C2.35523 9.25542 2.74458 9.64477 3.23463 9.84776C3.60218 10 4.06812 10 5 10H19C19.9319 10 20.3978 10 20.7654 9.84776C21.2554 9.64477 21.6448 9.25542 21.8478 8.76537C22 8.39782 22 7.93188 22 7C22 6.06812 22 5.60218 21.8478 5.23463C21.6448 4.74458 21.2554 4.35523 20.7654 4.15224C20.3978 4 19.9319 4 19 4Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M19 14H5C4.06812 14 3.60218 14 3.23463 14.1522C2.74458 14.3552 2.35523 14.7446 2.15224 15.2346C2 15.6022 2 16.0681 2 17C2 17.9319 2 18.3978 2.15224 18.7654C2.35523 19.2554 2.74458 19.6448 3.23463 19.8478C3.60218 20 4.06812 20 5 20H19C19.9319 20 20.3978 20 20.7654 19.8478C21.2554 19.6448 21.6448 19.2554 21.8478 18.7654C22 18.3978 22 17.9319 22 17C22 16.0681 22 15.6022 21.8478 15.2346C21.6448 14.7446 21.2554 14.3552 20.7654 14.1522C20.3978 14 19.9319 14 19 14Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M6.125 7H6M6.25 7C6.25 7.13807 6.13807 7.25 6 7.25C5.86193 7.25 5.75 7.13807 5.75 7C5.75 6.86193 5.86193 6.75 6 6.75C6.13807 6.75 6.25 6.86193 6.25 7Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M10.125 7H10M10.25 7C10.25 7.13807 10.1381 7.25 10 7.25C9.86193 7.25 9.75 7.13807 9.75 7C9.75 6.86193 9.86193 6.75 10 6.75C10.1381 6.75 10.25 6.86193 10.25 7Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M6.125 17H6M6.25 17C6.25 17.1381 6.13807 17.25 6 17.25C5.86193 17.25 5.75 17.1381 5.75 17C5.75 16.8619 5.86193 16.75 6 16.75C6.13807 16.75 6.25 16.8619 6.25 17Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M10.125 17H10M10.25 17C10.25 17.1381 10.1381 17.25 10 17.25C9.86193 17.25 9.75 17.1381 9.75 17C9.75 16.8619 9.86193 16.75 10 16.75C10.1381 16.75 10.25 16.8619 10.25 17Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/></svg>">
    Run your bot as a Linux daemon
  </Card>

  <Card title="PM2" href="/guides/ecosystem/pm2" icon="<svg xmlns=&#x22;http://www.w3.org/2000/svg&#x22; viewBox=&#x22;0 0 24 24&#x22; fill=&#x22;none&#x22;><path d=&#x22;M19.995 12C19.995 7.58172 16.4132 4 11.995 4C7.5767 4 3.99498 7.58172 3.99498 12C3.99498 16.4182 7.5767 20 11.995 20C16.4132 20 19.995 16.4182 19.995 12Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M13.995 12C13.995 10.8954 13.0995 10 11.995 10C10.8904 10 9.99498 10.8954 9.99498 12C9.99498 13.1046 10.8904 14 11.995 14C13.0995 14 13.995 13.1046 13.995 12Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M11.995 4V2M11.995 20V22M15.9957 5.0693L16.9957 3.33725M6.99566 20.6578L10.9967 13.7248M18.9257 7.99558L20.6577 6.99558M5.06929 15.9956L3.33724 16.9956M22 11.9949L13.995 11.9949M4.00003 11.9949H2.00003M18.9307 15.9955L20.6628 16.9955M5.07431 7.99554L3.34226 6.99554M16.0044 18.9256L17.0044 20.6577M7.00437 3.33718L10.9278 10.1435&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/></svg>">
    Manage your bot with PM2
  </Card>
</Columns>
