Getting started
composurejs builds Discord bots that receive only HTTP Interactions. There's no gateway connection, so a bot is a function from a request to a response that runs anywhere fetch does: Cloudflare Workers, Node, Bun.
Create a bot
pnpm create composurejs my-botPick Cloudflare Workers, Node or Bun. Each template has the same starting bot (/ping, /greet, a /counter with buttons) and tests.
Connect it to Discord
- In the Developer Portal, create an application for development (your Dev Application) and add its bot to a test server. Keep it separate from the one you deploy.
pnpm create composurejsasks for the Dev Application's id (General Information → Application ID) and the test server's id (Developer Mode → right-click the server → Copy Server ID), and writes them intovite.config.ts. If you skipped them, fill them in there:
composure({ dev: { application: "1100146565504565411", guild: "798662131062931547" } })- Copy
.env.exampleto.env.localand fill in the Dev Application's secrets:
| Variable | Where it comes from |
|---|---|
DISCORD_PUBLIC_KEY | General Information → Public Key |
DISCORD_TOKEN | Bot → Token |
- Run
pnpm dev. It serves the bot, opens a public tunnel, sets the Dev Application's Interactions Endpoint URL to it, and syncs the Commands to your test server. Type/counterin Discord and press the button. See Local development for what each step does.
Production's secrets go in .env.production.local, which pnpm dev never reads (Deploying).
The shape of a bot
An App lists the Commands a bot serves. That list is what answers Interactions and what Sync publishes to Discord.
import { App, command, option } from "composurejs"
export const greet = command({
name: "greet",
description: "Greet someone",
options: { user: option.user("Who to greet") },
handler: (c, { user }) => `${c.user.username} waves at ${user.displayName}`,
})
export const app = new App().command(greet)The handler's second argument is typed from options. Hover user above: it is a ResolvedUser, already resolved from the Interaction. Answers are return values: a string, a JSX message, a modal, or a deferred reply (Responses).
Buttons answer the same way, written where the button is:
export const Counter = ({ count }: { count: number }) => (
<Container>
<Text>## Count: {count}</Text>
<Row>
<Button onClick={() => <Counter count={count + 1} />}>+1</Button>
</Row>
</Container>
)The click updates the message with the markup it returns. There are no ids to choose and nothing to register; Handlers and Views explains how.