Discord

Lightweight, dependency-free, in-memory fake of the Discord REST API (v10) for testing code that uses the real discord.js client (and any HTTP client that speaks the Discord REST protocol).

Default port: 4655

Quick start

Start the server:

import { DiscordServer } from "./services/discord/src/server.js";

const server = new DiscordServer(4655);
await server.start();
// ... run your app/tests ...
await server.stop();

Point the real discord.js REST client at it. The client reads its base URL from the api option (it appends /v{version} itself), so override it to the parlel fake:

import { REST } from "discord.js"; // or "@discordjs/rest"
import { Routes } from "discord-api-types/v10";

const rest = new REST({ version: "10" })
  .setToken("parlel.test.discordbottoken");

// Override the API base so requests hit the parlel fake instead of discord.com:
rest.options.api = "http://127.0.0.1:4655/api";

// Who am I?
const me = await rest.get(Routes.user("@me"));
// me.id === "1000000000000000010", me.username === "parlelbot"

// Send a message
const msg = await rest.post(Routes.channelMessages("3000000000000000001"), {
  body: { content: "hello from parlel" },
});
// msg.content === "hello from parlel", msg.author.id === me.id

// Register a slash command
await rest.put(
  Routes.applicationCommands("1000000000000000001"),
  { body: [{ name: "ping", description: "Ping!" }] },
);

The full Client (with the WebSocket Gateway) is not emulated — see the unsupported table below. Use the REST client (or raw fetch) for REST flows, which is what client.guilds, channel.send(), command deployment, etc. use under the hood.

Every guild, channel, message, member, role, ban, emoji, webhook, invite, reaction, and thread is captured in memory and can be inspected via the /__parlel/* endpoints (see below). The whole world is resettable.

Wire protocol

Seeded fixtures

On startup / reset the world contains:

EntityIDNotes
Bot user (@me)1000000000000000010parlelbot, bot: true
Human user1000000000000000020alice
Application1000000000000000001oauth2/applications/@me
Guild2000000000000000001Parlel Guild, owned by the bot
@everyone role2000000000000000001role id == guild id
Text channel3000000000000000001general

Valid tokens: parlel.test.discordbottoken, parlel.test.discordbottoken.bot.

Implemented operations / endpoints

Gateway

OAuth2 / application identity

Users

Channels

Messages

Reactions

Pins

Threads

Guilds

Guild channels

Guild members

Guild roles

Guild bans

Guild emojis

Invites (top-level)

Webhooks

Application (slash) commands

parlel control / inspection endpoints (not part of Discord)

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatus
REST: users, channels, messages, reactions, pins✅ Supported
REST: reaction object shape (count_details, me_burst, burst_colors)✅ Supported
REST: guilds, members, roles, bans, emojis✅ Supported
REST: add-member semantics (201 created / 204 already a member)✅ Supported
REST: threads & thread members✅ Supported
REST: webhooks (create, execute, edit/delete messages)✅ Supported
REST: invites (channel + top-level)✅ Supported
REST: application / slash commands (global + guild, bulk)✅ Supported
Gateway info routes (/gateway, /gateway/bot)✅ Supported
OAuth2 application identity routes✅ Supported
Audit-log reason header◐ Accepted (ignored)
Discord error envelope + codes✅ Supported
Message list cursor pagination (before / after / around)◐ Accepted — only limit is honored (newest-first)
Bulk-delete 2–100 count / two-week age validation◐ Accepted — deletes the given ids, never 400s
WebSocket Gateway (real-time events: messageCreate, presence, etc.)✓ By design — Not emulated — REST only
Voice connections / voice state✓ By design — Not emulated
Stage instances, scheduled events, auto-moderation✓ By design — Not emulated
Real attachment/CDN file hosting⟳ Roadmap — Uploads parsed, files not served from a CDN
Real rate limiting / 429 backoff✓ By design — Never throttles — local tests run at full speed, zero cost
OAuth2 token exchange / authorization-code flow✓ By design — Not emulated

Error codes / shapes

All errors return the Discord envelope:

{ "message": "Unknown Channel", "code": 10003 }

Validation errors additionally include an errors object:

{
  "message": "Invalid Form Body",
  "code": 50035,
  "errors": {
    "name": { "_errors": [{ "code": "BASE_TYPE_REQUIRED", "message": "This field is required" }] }
  }
}
HTTPcodeMeaning
4010Missing or invalid Authorization token
40410003Unknown Channel
40410004Unknown Guild
40410006Unknown Invite
40410007Unknown Member
40410008Unknown Message
40410009Unknown Overwrite
40410011Unknown Role
40410013Unknown User
40410014Unknown Emoji
40410015Unknown Webhook
40410026Unknown Ban
40410063Unknown Command
40050035Invalid Form Body (validation failure)

Tests

tests/discord.test.ts starts the server on port 14655, drives every implemented operation (happy paths plus key edge cases) through a faithful discord.js-style REST client mirror, and tears the server down in afterAll. Run it with:

npx vitest run tests/discord.test.ts
<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

DISCORD_TOKEN=parlel.test.discordbottoken
DISCORD_BOT_TOKEN=parlel.test.discordbottoken
DISCORD_APPLICATION_ID=1000000000000000001
DISCORD_API_BASE_URL=http://parlel-bridge:4655/api/v10
<!-- parlel:testenv:end -->