elysiajs-ddd

ElysiaJS Domain-Driven Design Expert

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "elysiajs-ddd" with this command: npx skills add 0xkynz/codekit/0xkynz-codekit-elysiajs-ddd

ElysiaJS Domain-Driven Design Expert

You are an expert in ElysiaJS, Domain-Driven Design (DDD), Prisma ORM, Better Auth, and Bun runtime. You help build scalable, maintainable backend APIs with clean architecture principles.

Core Architecture

DDD Folder Structure

src/ ├── domains/ # Bounded contexts by business domain │ ├── user/ # User domain example │ │ ├── domain/ # Core domain logic (framework-agnostic) │ │ │ ├── entities/ │ │ │ │ └── User.ts │ │ │ ├── value-objects/ │ │ │ │ └── Email.ts │ │ │ ├── services/ │ │ │ │ └── UserDomainService.ts │ │ │ ├── events/ │ │ │ │ └── UserCreatedEvent.ts │ │ │ └── types.ts │ │ ├── application/ # Use cases and application services │ │ │ ├── commands/ │ │ │ │ └── CreateUserCommand.ts │ │ │ ├── queries/ │ │ │ │ └── GetUserQuery.ts │ │ │ └── services/ │ │ │ └── UserApplicationService.ts │ │ ├── infrastructure/ # External adapters (DB, HTTP) │ │ │ ├── repositories/ │ │ │ │ └── PrismaUserRepository.ts │ │ │ └── controllers/ │ │ │ └── userController.ts │ │ └── index.ts # Domain module export │ ├── order/ # Order domain (example) │ └── payment/ # Payment domain (example) ├── shared/ # Cross-cutting concerns │ ├── domain/ │ │ ├── Entity.ts # Base entity class │ │ ├── ValueObject.ts # Base value object class │ │ ├── AggregateRoot.ts │ │ └── DomainEvent.ts │ ├── infrastructure/ │ │ ├── prisma.ts # Prisma client singleton │ │ ├── auth.ts # Better Auth setup │ │ └── logger.ts │ └── kernel/ # Repository interfaces, base types │ └── repositories/ │ └── Repository.ts ├── modules/ # Elysia entry points (thin controllers) │ ├── userModule.ts │ └── index.ts └── index.ts # App entry point

Layer Responsibilities

  1. Domain Layer (Pure Business Logic)

Entities - Objects with identity:

// domains/user/domain/entities/User.ts import { Entity } from '@/shared/domain/Entity'; import { Email } from '../value-objects/Email';

export interface UserProps { email: Email; name: string; passwordHash: string; createdAt: Date; updatedAt: Date; }

export class User extends Entity<UserProps> { get email(): Email { return this.props.email; }

get name(): string { return this.props.name; }

updateEmail(newEmail: Email): void { this.props.email = newEmail; this.props.updatedAt = new Date(); }

updateName(newName: string): void { if (newName.length < 2) { throw new Error('Name must be at least 2 characters'); } this.props.name = newName; this.props.updatedAt = new Date(); }

static create(props: Omit<UserProps, 'createdAt' | 'updatedAt'>, id?: string): User { return new User({ ...props, createdAt: new Date(), updatedAt: new Date(), }, id); } }

Value Objects - Immutable objects without identity:

// domains/user/domain/value-objects/Email.ts import { ValueObject } from '@/shared/domain/ValueObject';

interface EmailProps { value: string; }

export class Email extends ValueObject<EmailProps> { get value(): string { return this.props.value; }

private static isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+.[^\s@]+$/; return emailRegex.test(email); }

static create(email: string): Email { if (!this.isValidEmail(email)) { throw new Error('Invalid email format'); } return new Email({ value: email.toLowerCase() }); } }

Domain Services - Business logic that doesn't belong to entities:

// domains/user/domain/services/UserDomainService.ts import { User } from '../entities/User';

export class UserDomainService { static async hashPassword(password: string): Promise<string> { return await Bun.password.hash(password); }

static async verifyPassword(password: string, hash: string): Promise<boolean> { return await Bun.password.verify(password, hash); }

static validatePasswordStrength(password: string): boolean { return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password); } }

  1. Application Layer (Use Cases)

Commands - Write operations:

// domains/user/application/commands/CreateUserCommand.ts import { User } from '../../domain/entities/User'; import { Email } from '../../domain/value-objects/Email'; import { UserDomainService } from '../../domain/services/UserDomainService'; import type { UserRepository } from '@/shared/kernel/repositories/UserRepository';

export interface CreateUserInput { email: string; name: string; password: string; }

export class CreateUserCommand { constructor(private readonly userRepository: UserRepository) {}

async execute(input: CreateUserInput): Promise<User> { // Validate password strength (domain rule) if (!UserDomainService.validatePasswordStrength(input.password)) { throw new Error('Password does not meet strength requirements'); }

// Check if email already exists
const existingUser = await this.userRepository.findByEmail(input.email);
if (existingUser) {
  throw new Error('User with this email already exists');
}

// Create domain objects
const email = Email.create(input.email);
const passwordHash = await UserDomainService.hashPassword(input.password);

// Create user entity
const user = User.create({
  email,
  name: input.name,
  passwordHash,
});

// Persist
await this.userRepository.save(user);

return user;

} }

Queries - Read operations:

// domains/user/application/queries/GetUserQuery.ts import type { User } from '../../domain/entities/User'; import type { UserRepository } from '@/shared/kernel/repositories/UserRepository';

export class GetUserQuery { constructor(private readonly userRepository: UserRepository) {}

async execute(userId: string): Promise<User | null> { return this.userRepository.findById(userId); }

async executeByEmail(email: string): Promise<User | null> { return this.userRepository.findByEmail(email); } }

  1. Infrastructure Layer (Adapters)

Repository Implementation (Prisma):

// domains/user/infrastructure/repositories/PrismaUserRepository.ts import type { PrismaClient } from '@prisma/client'; import { User } from '../../domain/entities/User'; import { Email } from '../../domain/value-objects/Email'; import type { UserRepository } from '@/shared/kernel/repositories/UserRepository';

export class PrismaUserRepository implements UserRepository { constructor(private readonly prisma: PrismaClient) {}

async save(user: User): Promise<void> { await this.prisma.user.upsert({ where: { id: user.id }, update: { email: user.email.value, name: user.name, updatedAt: new Date(), }, create: { id: user.id, email: user.email.value, name: user.name, passwordHash: user.props.passwordHash, createdAt: user.props.createdAt, updatedAt: user.props.updatedAt, }, }); }

async findById(id: string): Promise<User | null> { const data = await this.prisma.user.findUnique({ where: { id } }); return data ? this.toDomain(data) : null; }

async findByEmail(email: string): Promise<User | null> { const data = await this.prisma.user.findUnique({ where: { email: email.toLowerCase() } }); return data ? this.toDomain(data) : null; }

async delete(id: string): Promise<void> { await this.prisma.user.delete({ where: { id } }); }

private toDomain(data: any): User { return new User({ email: Email.create(data.email), name: data.name, passwordHash: data.passwordHash, createdAt: data.createdAt, updatedAt: data.updatedAt, }, data.id); } }

  1. Module Layer (Elysia Controllers)

// modules/userModule.ts import { Elysia, t } from 'elysia'; import { CreateUserCommand } from '@/domains/user/application/commands/CreateUserCommand'; import { GetUserQuery } from '@/domains/user/application/queries/GetUserQuery'; import { PrismaUserRepository } from '@/domains/user/infrastructure/repositories/PrismaUserRepository'; import { prisma } from '@/shared/infrastructure/prisma'; import { auth } from '@/shared/infrastructure/auth';

// Create repository instance const userRepository = new PrismaUserRepository(prisma);

export const userModule = new Elysia({ prefix: '/users' }) // Auth guard using derive .derive(async ({ headers }) => { const session = await auth.api.getSession({ headers }); return { session }; })

// Create user .post('/', async ({ body }) => { const command = new CreateUserCommand(userRepository); const user = await command.execute(body); return { id: user.id, email: user.email.value, name: user.name, }; }, { body: t.Object({ email: t.String({ format: 'email' }), name: t.String({ minLength: 2 }), password: t.String({ minLength: 8 }), }), })

// Get user by ID .get('/:id', async ({ params, session, status }) => { if (!session) { return status(401, { error: 'Unauthorized' }); }

const query = new GetUserQuery(userRepository);
const user = await query.execute(params.id);

if (!user) {
  return status(404, { error: 'User not found' });
}

return {
  id: user.id,
  email: user.email.value,
  name: user.name,
};

}, { params: t.Object({ id: t.String(), }), })

// Guard for protected routes .guard({ beforeHandle: ({ session, status }) => { if (!session) { return status(401, { error: 'Unauthorized' }); } }, }) .get('/me', async ({ session }) => { const query = new GetUserQuery(userRepository); const user = await query.execute(session!.user.id); return user ? { id: user.id, email: user.email.value, name: user.name, } : null; });

Shared Infrastructure

Prisma Setup

// shared/infrastructure/prisma.ts import { PrismaClient } from '@prisma/client';

declare global { var prisma: PrismaClient | undefined; }

export const prisma = globalThis.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query'] : [], });

if (process.env.NODE_ENV !== 'production') { globalThis.prisma = prisma; }

Better Auth Setup

// shared/infrastructure/auth.ts import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; import { prisma } from './prisma';

export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: 'postgresql', // or 'mysql', 'sqlite' }), emailAndPassword: { enabled: true, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day }, });

Auth Module (Mount Better Auth)

// modules/authModule.ts import { Elysia } from 'elysia'; import { auth } from '@/shared/infrastructure/auth';

export const authModule = new Elysia({ prefix: '/auth' }) .mount(auth.handler);

Base Classes

Entity Base

// shared/domain/Entity.ts import { randomUUID } from 'crypto';

export abstract class Entity<T> { protected readonly _id: string; protected props: T;

constructor(props: T, id?: string) { this._id = id ?? randomUUID(); this.props = props; }

get id(): string { return this._id; }

equals(entity: Entity<T>): boolean { return this._id === entity._id; } }

Value Object Base

// shared/domain/ValueObject.ts export abstract class ValueObject<T> { protected readonly props: T;

constructor(props: T) { this.props = Object.freeze(props); }

equals(vo: ValueObject<T>): boolean { return JSON.stringify(this.props) === JSON.stringify(vo.props); } }

Repository Interface

// shared/kernel/repositories/Repository.ts export interface Repository<T> { save(entity: T): Promise<void>; findById(id: string): Promise<T | null>; delete(id: string): Promise<void>; }

// shared/kernel/repositories/UserRepository.ts import type { User } from '@/domains/user/domain/entities/User'; import type { Repository } from './Repository';

export interface UserRepository extends Repository<User> { findByEmail(email: string): Promise<User | null>; }

App Entry Point

// index.ts import { Elysia } from 'elysia'; import { openapi } from '@elysiajs/openapi'; import { cors } from '@elysiajs/cors'; import { userModule } from './modules/userModule'; import { authModule } from './modules/authModule';

const app = new Elysia() .use(cors()) .use(openapi()) .use(authModule) .use(userModule) .get('/health', () => ({ status: 'ok' })) .listen(3000);

console.log(Server running at http://localhost:${app.server?.port});

export type App = typeof app;

Prisma Schema with Prismabox

// prisma/schema.prisma generator client { provider = "prisma-client-js" output = "../generated/prisma" }

generator prismabox { provider = "prismabox" output = "../generated/prismabox" }

datasource db { provider = "postgresql" url = env("DATABASE_URL") }

model User { id String @id @default(uuid()) email String @unique name String passwordHash String @map("password_hash") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at")

@@map("users") }

Key Principles

  • Domain Layer is Pure - No framework dependencies, no I/O

  • Dependency Inversion - Domain defines interfaces, infrastructure implements

  • Use Cases Orchestrate - Application layer coordinates domain logic

  • Controllers are Thin - Only HTTP concerns, delegate to use cases

  • Repository Pattern - Abstract data access behind interfaces

Common Commands

Initialize project

bun init bun add elysia @elysiajs/openapi @elysiajs/cors bun add @prisma/client prismabox better-auth bun add -d prisma typescript @types/bun

Prisma commands

bunx prisma init bunx prisma generate bunx prisma db push bunx prisma migrate dev --name init

Run development

bun run --watch src/index.ts

Build for production

bun build src/index.ts --outdir dist --target bun

Testing Strategy

// domains/user/application/commands/tests/CreateUserCommand.test.ts import { describe, it, expect, mock } from 'bun:test'; import { CreateUserCommand } from '../CreateUserCommand';

describe('CreateUserCommand', () => { it('should create a user with valid input', async () => { const mockRepo = { findByEmail: mock(() => Promise.resolve(null)), save: mock(() => Promise.resolve()), };

const command = new CreateUserCommand(mockRepo as any);
const user = await command.execute({
  email: 'test@example.com',
  name: 'Test User',
  password: 'Password123',
});

expect(user.email.value).toBe('test@example.com');
expect(mockRepo.save).toHaveBeenCalled();

}); });

Error Handling

// shared/domain/errors/DomainError.ts export class DomainError extends Error { constructor(message: string) { super(message); this.name = 'DomainError'; } }

export class NotFoundError extends DomainError { constructor(entity: string, id: string) { super(${entity} with id ${id} not found); this.name = 'NotFoundError'; } }

export class ValidationError extends DomainError { constructor(message: string) { super(message); this.name = 'ValidationError'; } }

// Global error handler in Elysia const app = new Elysia() .onError(({ error, set }) => { if (error.name === 'NotFoundError') { set.status = 404; return { error: error.message }; } if (error.name === 'ValidationError') { set.status = 400; return { error: error.message }; } set.status = 500; return { error: 'Internal server error' }; });

Eden Client (Type-Safe Frontend)

// On frontend import { treaty } from '@elysiajs/eden'; import type { App } from './server';

const api = treaty<App>('localhost:3000');

// Fully typed API calls const { data, error } = await api.users({ id: '123' }).get();

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

uiux-design-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

react-native-expo

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

figma-make-website-builder

No summary provided by upstream source.

Repository SourceNeeds Review