Declarative embeds
The embed system in @spraxium/components lets you define Discord embeds as decorated classes instead of imperative builder chains. Each embed property, title, color, fields, footer, images, is declared through property-level decorators. At render time, EmbedService reads the metadata and produces a standard discord.js EmbedBuilder populated with all declared values.
This approach brings the same benefits as the button and select patterns: definitions are reusable, fields are testable independently, and rendering is decoupled from business logic. Data-driven embeds are especially powerful because factory functions on each decorator can compute values at render time based on the data object you pass.
The @Embed decorator marks a class as an embed schema. It optionally accepts a static EmbedSchema object with top-level properties like title, description, color, footer, and url. For properties that need runtime data, use property-level decorators like @EmbedTitle, @EmbedColor, and @EmbedField instead.
Basic embed with static properties
The simplest embed uses the @Embed class decorator with a static schema. This is suitable for embeds whose content does not change at runtime. The schema object accepts the same fields as the discord.js EmbedBuilder API: title, description, color, footer, timestamp, author, thumbnail, image, and url.
import { Embed } from '@spraxium/components';
@Embed({
title: 'Welcome',
description: 'Welcome to the server! Please read the rules.',
color: 0x5865f2,
footer: { text: 'Spraxium Bot' },
timestamp: true,
})
export class WelcomeEmbed {}To render a static embed, inject EmbedService and call build with the class. The service reads the @Embed metadata and produces the configured EmbedBuilder.
Data-driven embeds with decorators
For embeds that depend on runtime data, use property-level decorators. Each decorator accepts either a static value or a factory function that receives the data object. This lets you compute titles, colors, fields, and other properties dynamically while keeping the declaration in one place.
import {
Embed,
EmbedTitle,
EmbedColor,
EmbedField,
EmbedThumbnail,
EmbedFooter,
} from '@spraxium/components';
interface ProfileData {
username: string;
level: number;
isPremium: boolean;
avatarUrl: string;
}
@Embed()
export class UserProfileEmbed {
@EmbedTitle<ProfileData>((data) => `${data.username}'s Profile`)
title!: never;
@EmbedColor<ProfileData>((data) => (data.isPremium ? 0xffd700 : 0x5865f2))
color!: never;
@EmbedThumbnail<ProfileData>((data) => data.avatarUrl)
thumbnail!: never;
@EmbedField<ProfileData>({
name: 'Level',
value: (data) => String(data.level),
inline: true,
})
level!: never;
@EmbedField<ProfileData>({
name: 'Status',
value: (data) => (data.isPremium ? '⭐ Premium' : 'Free'),
inline: true,
})
status!: never;
@EmbedFooter<ProfileData>((data) => ({ text: `ID: ${data.username}` }))
footer!: never;
}The generic type parameter on each decorator provides type safety for the factory function's argument. The property declaration with !: never is a TypeScript convention that satisfies the compiler without creating actual instance state, since the embed values are computed from metadata, not from stored properties.
The @EmbedDescription decorator's factory function can return either a plain string or a DescriptionBuilder instance. The desc() shorthand creates a builder that chains markdown formatting methods, producing structured descriptions without string concatenation or template-literal noise.
import {
Embed,
EmbedTitle,
EmbedDescription,
EmbedColor,
desc,
} from '@spraxium/components';
interface AccountData {
username: string;
balance: number;
isPremium: boolean;
}
@Embed()
export class AccountSummaryEmbed {
@EmbedTitle<AccountData>((data) => `${data.username}'s Account`)
title!: never;
@EmbedDescription<AccountData>((data) =>
desc()
.bold(`Balance: ${data.balance} coins`)
.empty()
.when(data.isPremium, (b) =>
b.quote("\u2b50 Premium \u2014 2\u00d7 rewards active"),
)
.build(),
)
description!: never;
@EmbedColor<AccountData>((data) => (data.isPremium ? 0xffd700 : 0x5865f2))
color!: never;
}Embed property decorators
Each property decorator controls a specific aspect of the embed's appearance. All accept either a static value or a factory function that receives the data object at render time.
| Decorator | Value type | Purpose |
|---|---|---|
| @EmbedTitle | String or function returning a string | Sets the embed title. Factory version can personalize per render. |
| @EmbedDescription | String or function returning a string | Overrides the description from the @Embed schema with a dynamic value. |
| @EmbedColor | Number, string, or function returning either | Hex color code or named color. Factory enables conditional coloring. |
| @EmbedFooter | String, FooterConfig, or function returning either | Footer text and optional icon URL. |
| @EmbedTimestamp | Boolean, Date, or function returning either | When true, uses current time. Accepts a Date for specific timestamps. |
| @EmbedAuthor | AuthorConfig or function returning it | Author name, icon URL, and optional link. |
| @EmbedThumbnail | String or function returning a string | Small image URL displayed in the top-right corner. |
| @EmbedImage | String or function returning a string | Main image URL displayed at the bottom of the embed. |
| @EmbedUrl | String or function returning a string | Makes the embed title a clickable link. |
Embed fields
The @EmbedField decorator registers a field with a name-value pair on the embed. Fields appear as structured rows below the description. The value property can be a static string or a factory function for dynamic content. The inline property controls whether the field shares a row with adjacent inline fields.
import { Embed, EmbedTitle, EmbedField } from '@spraxium/components';
interface StatsData {
memberCount: number;
onlineCount: number;
boostLevel: number;
}
@Embed({ color: 0x00bfff })
export class ServerStatsEmbed {
@EmbedTitle('Server Statistics')
title!: never;
@EmbedField<StatsData>({
name: 'Members',
value: (d) => String(d.memberCount),
inline: true,
})
members!: never;
@EmbedField<StatsData>({
name: 'Online',
value: (d) => String(d.onlineCount),
inline: true,
})
online!: never;
@EmbedField<StatsData>({
name: 'Boost Level',
value: (d) => `Level ${d.boostLevel}`,
inline: true,
})
boost!: never;
}Conditional fields
The @EmbedWhen decorator conditionally includes a field based on a predicate function. The predicate receives the data object and returns a boolean. When the predicate returns false, the field is omitted from the rendered embed entirely. This is useful for fields that should only appear for certain user roles, subscription tiers, or data states.
import { Embed, EmbedField, EmbedWhen } from '@spraxium/components';
interface ProfileData {
username: string;
isPremium: boolean;
premiumSince?: string;
}
@Embed({ title: 'Profile' })
export class PremiumProfileEmbed {
@EmbedField<ProfileData>({ name: 'User', value: (d) => d.username })
user!: never;
@EmbedWhen<ProfileData>((d) => d.isPremium)
@EmbedField<ProfileData>({
name: 'Premium Since',
value: (d) => d.premiumSince ?? 'Unknown',
})
premiumField!: never;
}Always apply @EmbedWhen before @EmbedField on the same property. The decorator stacking order matters, if the predicate returns false, the field metadata is skipped entirely during rendering.
DescriptionBuilder
The DescriptionBuilder provides a fluent API for composing rich markdown descriptions. It handles heading levels, bold text, italic text, code blocks, lists, and line breaks. The builder is useful when the embed description requires structured formatting that would be cumbersome to build with string concatenation.
import { DescriptionBuilder } from '@spraxium/components';
const description = new DescriptionBuilder()
.heading('Commands', 2)
.bold('!help')
.text(' - Shows this message')
.newline()
.bold('!ping')
.text(' - Check bot latency')
.newline()
.heading('Links', 2)
.link('Documentation', 'https://docs.example.com')
.build();Rendering with EmbedService
EmbedService is the injectable service that transforms decorated embed classes into discord.js EmbedBuilder objects. The build method accepts the class and an optional data object. It also provides convenience methods for common embed patterns.
| Method | Purpose |
|---|---|
| build | Renders a decorated embed class into an EmbedBuilder with the given data. |
| simple | Creates a simple embed with a description and optional color, without needing a class. |
| error | Creates a red-themed error embed with a description. |
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { EmbedService } from '@spraxium/components';
import type { ChatInputCommandInteraction } from 'discord.js';
import { UserProfileEmbed } from '../components/embeds/user-profile.embed';
import { ProfileCommand } from './profile.command';
@SlashCommandHandler(ProfileCommand)
export class ProfileCommandHandler {
constructor(private readonly embeds: EmbedService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const profileData = {
username: interaction.user.username,
level: 42,
isPremium: true,
avatarUrl: interaction.user.displayAvatarURL(),
};
const embed = this.embeds.build(UserProfileEmbed, profileData);
await interaction.reply({ embeds: [embed] });
}
}Module registration
Embed classes do not require any module registration. EmbedService is provided globally by ComponentsModule and is injectable in any command handler without additional wiring. Pass the embed class as a direct argument to embeds.build() at call time. The service reads decorator metadata directly from the class constructor, not from DI. The feature module only needs to list the command and handler classes that use the service.
import { Module } from '@spraxium/common';
import { ProfileCommand } from './commands/profile.command';
@Module({
commands: [ProfileCommand],
})
export class ProfileModule {}