| name | elysiajs-ddd-mongoose |
| description | ElysiaJS with Domain-Driven Design architecture, MongoDB, Mongoose ODM, Better Auth, and Bun runtime. Use PROACTIVELY when building backend APIs with ElysiaJS and MongoDB, implementing DDD patterns with document databases. |
ElysiaJS Domain-Driven Design with MongoDB Expert
You are an expert in ElysiaJS, Domain-Driven Design (DDD), MongoDB, Mongoose ODM, Better Auth, and Bun runtime. You help build scalable, maintainable backend APIs with clean architecture principles using document databases.
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
│ │ │ ├── aggregates/
│ │ │ │ └── UserAggregate.ts
│ │ │ ├── services/
│ │ │ │ └── UserDomainService.ts
│ │ │ └── types.ts
│ │ ├── application/ # Use cases and application services
│ │ │ ├── commands/
│ │ │ │ └── CreateUserCommand.ts
│ │ │ ├── queries/
│ │ │ │ └── GetUserQuery.ts
│ │ │ └── services/
│ │ │ └── UserApplicationService.ts
│ │ ├── infrastructure/ # External adapters (DB, HTTP)
│ │ │ ├── repositories/
│ │ │ │ └── MongoUserRepository.ts
│ │ │ ├── schemas/
│ │ │ │ └── userSchema.ts
│ │ │ └── controllers/
│ │ │ └── userController.ts
│ │ └── index.ts # Domain module export
│ ├── product/ # Product domain (example)
│ └── order/ # Order domain (example)
├── shared/ # Cross-cutting concerns
│ ├── domain/
│ │ ├── Entity.ts # Base entity class
│ │ ├── ValueObject.ts # Base value object class
│ │ ├── AggregateRoot.ts
│ │ └── DomainEvent.ts
│ ├── infrastructure/
│ │ ├── mongodb.ts # MongoDB connection
│ │ ├── 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
MongoDB + Mongoose Setup
Connection Configuration
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp';
declare global {
var mongooseConnection: typeof mongoose | undefined;
}
export async function connectDB(): Promise<typeof mongoose> {
if (globalThis.mongooseConnection) {
return globalThis.mongooseConnection;
}
try {
const connection = await mongoose.connect(MONGODB_URI, {
bufferCommands: false,
});
if (process.env.NODE_ENV !== 'production') {
globalThis.mongooseConnection = connection;
}
console.log('MongoDB connected successfully');
return connection;
} catch (error) {
console.error('MongoDB connection error:', error);
throw error;
}
}
export (): <> {
mongoose.();
globalThis. = ;
}
{ mongoose };
Mongoose Schema Definition
import { Schema, model, Document, Types } from 'mongoose';
export interface IUserDocument extends Document {
_id: Types.ObjectId;
email: string;
name: string;
passwordHash: string;
role: 'user' | 'admin';
profile: {
avatar?: string;
bio?: string;
};
preferences: {
notifications: boolean;
theme: 'light' | 'dark';
};
createdAt: Date;
updatedAt: Date;
}
const userSchema = new Schema<IUserDocument>(
{
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
},
: {
: ,
: [, ],
: ,
},
: {
: ,
: { : , : },
},
: {
: { : , : },
: { : , : [, ], : },
},
},
{
: ,
: {
: ,
: {
ret. = ret..();
ret.;
ret.;
ret.;
ret;
},
},
}
);
userSchema.({ : - });
userSchema.({ : , : });
userSchema.().( () {
.;
});
userSchema.. = (): {
. === ;
};
userSchema.. = () {
.({ : email.() });
};
= model<>(, userSchema);
Domain Layer (Pure Business Logic)
Entity Base Class
import { Types } from 'mongoose';
export abstract class Entity<T> {
protected readonly _id: string;
protected props: T;
constructor(props: T, id?: string) {
this._id = id ?? new Types.ObjectId().toString();
this.props = props;
}
get id(): string {
return this._id;
}
equals(entity: Entity<T>): boolean {
return this._id === entity._id;
}
}
Value Object Base Class
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);
}
}
User Entity
import { Entity } from '@/shared/domain/Entity';
import { Email } from '../value-objects/Email';
export interface UserProps {
email: Email;
name: string;
passwordHash: string;
role: 'user' | 'admin';
profile: {
avatar?: string;
bio?: string;
};
preferences: {
notifications: boolean;
theme: 'light' | 'dark';
};
createdAt: Date;
updatedAt: Date;
}
export class User extends Entity<UserProps> {
get email(): Email {
return this.props.email;
}
get name(): string {
return ..;
}
(): | {
..;
}
() {
..;
}
() {
..;
}
(): {
.. === ;
}
(: ): {
.. = newEmail;
.. = ();
}
(: ): {
(newName. < ) {
();
}
.. = newName;
.. = ();
}
(: <[]>): {
.. = { ....., ...profile };
.. = ();
}
(: <[]>): {
.. = { ....., ...preferences };
.. = ();
}
(): {
.. = ;
.. = ();
}
(
: <, | | | | > &
<<, | | >>,
?:
): {
({
...props,
: props. ?? ,
: props. ?? {},
: props. ?? { : , : },
: (),
: (),
}, id);
}
}
Email Value Object
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().trim() });
}
}
Domain Service
export class UserDomainService {
static async hashPassword(password: string): Promise<string> {
return await Bun.password.hash(password, {
algorithm: 'argon2id',
memoryCost: 65536,
timeCost: 3,
});
}
static async verifyPassword(password: string, hash: string): Promise<boolean> {
return await Bun.password.verify(password, hash);
}
static validatePasswordStrength(password: string): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
(!.(password)) {
errors.();
}
(!.(password)) {
errors.();
}
(!.(password)) {
errors.();
}
{ : errors. === , errors };
}
}
Application Layer (Use Cases)
Create User Command
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 interface CreateUserOutput {
id: string;
email: string;
name: string;
role: string;
}
export class CreateUserCommand {
constructor(private readonly userRepository: UserRepository) {}
async execute(input: CreateUserInput): Promise<> {
passwordValidation = .(input.);
(!passwordValidation.) {
(passwordValidation..());
}
existingUser = ..(input.);
(existingUser) {
();
}
email = .(input.);
passwordHash = .(input.);
user = .({
email,
: input.,
passwordHash,
});
..(user);
{
: user.,
: user..,
: user.,
: user.,
};
}
}
Update User Command
import { Email } from '../../domain/value-objects/Email';
import type { UserRepository } from '@/shared/kernel/repositories/UserRepository';
export interface UpdateUserInput {
userId: string;
name?: string;
email?: string;
profile?: {
avatar?: string;
bio?: string;
};
preferences?: {
notifications?: boolean;
theme?: 'light' | 'dark';
};
}
export class UpdateUserCommand {
constructor(private readonly userRepository: UserRepository) {}
async execute(input: UpdateUserInput): Promise<void> {
const user = await this.userRepository.findById(input.userId);
if (!user) {
();
}
(input.) {
user.(input.);
}
(input.) {
newEmail = .(input.);
existingUser = ..(input.);
(existingUser && existingUser. !== user.) {
();
}
user.(newEmail);
}
(input.) {
user.(input.);
}
(input.) {
user.(input.);
}
..(user);
}
}
Get User Query
import type { User } from '../../domain/entities/User';
import type { UserRepository } from '@/shared/kernel/repositories/UserRepository';
export interface UserDTO {
id: string;
email: string;
name: string;
role: string;
profile: {
avatar?: string;
bio?: string;
};
preferences: {
notifications: boolean;
theme: 'light' | 'dark';
};
createdAt: Date;
}
export class GetUserQuery {
constructor(private readonly userRepository: UserRepository) {}
async execute(userId: string): Promise<UserDTO | null> {
const user = await ..(userId);
user ? .(user) : ;
}
(: ): < | > {
user = ..(email);
user ? .(user) : ;
}
(: ): {
{
: user.,
: user..,
: user.,
: user.,
: user.,
: user.,
: user..,
};
}
}
Search Users Query
import type { UserRepository, SearchOptions } from '@/shared/kernel/repositories/UserRepository';
import type { UserDTO } from './GetUserQuery';
export interface SearchUsersInput {
query?: string;
role?: 'user' | 'admin';
page?: number;
limit?: number;
sortBy?: 'name' | 'email' | 'createdAt';
sortOrder?: 'asc' | 'desc';
}
export interface SearchUsersOutput {
users: UserDTO[];
total: number;
page: number;
totalPages: number;
}
export class SearchUsersQuery {
constructor(private readonly userRepository: UserRepository) {}
async execute(: ): <> {
page = input. ?? ;
limit = input. ?? ;
: = {
: input.,
: input.,
: (page - ) * limit,
limit,
: input. ?? ,
: input. ?? ,
};
{ users, total } = ..(options);
{
: users.( ({
: user.,
: user..,
: user.,
: user.,
: user.,
: user.,
: user..,
})),
total,
page,
: .(total / limit),
};
}
}
Infrastructure Layer (MongoDB Repository)
Repository Interface
import type { User } from '@/domains/user/domain/entities/User';
import type { Repository } from './Repository';
export interface SearchOptions {
query?: string;
role?: 'user' | 'admin';
skip?: number;
limit?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
export interface SearchResult<T> {
users: T[];
total: number;
}
export interface UserRepository extends Repository<User> {
findByEmail(email: string): Promise<User | null>;
search(options: SearchOptions): Promise<SearchResult<User>>;
countByRole(: | ): <>;
}
MongoDB Repository Implementation
import { User } from '../../domain/entities/User';
import { Email } from '../../domain/value-objects/Email';
import { UserModel, type IUserDocument } from '../schemas/userSchema';
import type { UserRepository, SearchOptions, SearchResult } from '@/shared/kernel/repositories/UserRepository';
import type { FilterQuery } from 'mongoose';
export class MongoUserRepository implements UserRepository {
async save(user: User): Promise<void> {
const data = this.toPersistence(user);
await UserModel.findByIdAndUpdate(
user.id,
{ $set: data },
{ upsert: true, new: true }
);
}
(: ): < | > {
doc = .(id).();
doc ? .(doc) : ;
}
(: ): < | > {
doc = .({
: email.()
}).();
doc ? .(doc) : ;
}
(: ): <> {
.(id);
}
(: ): <<>> {
: <> = {};
(options.) {
filter. = { : options. };
}
(options.) {
filter. = options.;
}
: <, | -> = {};
(options.) {
sort[options.] = options. === ? : -;
}
[docs, total] = .([
.(filter)
.(sort)
.(options. ?? )
.(options. ?? )
.(),
.(filter),
]);
{
: docs.( .(doc)),
total,
};
}
(: | ): <> {
.({ role });
}
(: ): <> {
count = .({ : id });
count > ;
}
(: ): {
(
{
: .(doc.),
: doc.,
: doc.,
: doc.,
: doc. ?? {},
: doc. ?? { : , : },
: doc.,
: doc.,
},
doc..()
);
}
(: ): <> {
{
: user. ,
: user..,
: user.,
: user..,
: user.,
: user.,
: user.,
: (),
};
}
}
Module Layer (Elysia Controllers)
User Module
import { Elysia, t } from 'elysia';
import { CreateUserCommand } from '@/domains/user/application/commands/CreateUserCommand';
import { UpdateUserCommand } from '@/domains/user/application/commands/UpdateUserCommand';
import { GetUserQuery } from '@/domains/user/application/queries/GetUserQuery';
import { SearchUsersQuery } from '@/domains/user/application/queries/SearchUsersQuery';
import { MongoUserRepository } from '@/domains/user/infrastructure/repositories/MongoUserRepository';
import { auth } from '@/shared/infrastructure/auth';
const userRepository = new MongoUserRepository();
export const userModule = new Elysia({ prefix: '/users' })
.derive(async ({ headers }) => {
const session = await auth.api.getSession({ headers });
return { session };
})
.(, ({ body, status }) => {
{
command = (userRepository);
command.(body);
} (error) {
(error ) {
(, { : error. });
}
error;
}
}, {
: t.({
: t.({ : }),
: t.({ : , : }),
: t.({ : }),
}),
})
.(, ({ query, session, status }) => {
(!session) {
(, { : });
}
searchQuery = (userRepository);
searchQuery.({
: query.,
: query. | | ,
: query. ? (query.) : ,
: query. ? (query.) : ,
: query. | | | ,
: query. | | ,
});
}, {
: t.({
: t.(t.()),
: t.(t.()),
: t.(t.()),
: t.(t.()),
: t.(t.()),
: t.(t.()),
}),
})
.(, ({ params, session, status }) => {
(!session) {
(, { : });
}
query = (userRepository);
user = query.(params.);
(!user) {
(, { : });
}
user;
}, {
: t.({
: t.(),
}),
})
.(, ({ params, body, session, status }) => {
(!session) {
(, { : });
}
(session.. !== params. && session.. !== ) {
(, { : });
}
{
command = (userRepository);
command.({
: params.,
...body,
});
{ : };
} (error) {
(error ) {
(, { : error. });
}
error;
}
}, {
: t.({
: t.(),
}),
: t.({
: t.(t.({ : , : })),
: t.(t.({ : })),
: t.(t.({
: t.(t.()),
: t.(t.({ : })),
})),
: t.(t.({
: t.(t.()),
: t.(t.([t.(), t.()])),
})),
}),
})
.(, ({ params, session, status }) => {
(!session) {
(, { : });
}
(session.. !== ) {
(, { : });
}
userRepository.(params.);
{ : };
}, {
: t.({
: t.(),
}),
})
.(, ({ session, status }) => {
(!session) {
(, { : });
}
query = (userRepository);
query.(session..);
});
Better Auth Setup with MongoDB
import { betterAuth } from 'better-auth';
import { mongodbAdapter } from 'better-auth/adapters/mongodb';
import { mongoose } from './mongodb';
export const auth = betterAuth({
database: mongodbAdapter(mongoose.connection.getClient()),
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
},
session: {
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
},
user: {
additionalFields: {
role: {
type: 'string',
defaultValue: 'user',
},
},
},
});
Auth Module
import { Elysia } from 'elysia';
import { auth } from '@/shared/infrastructure/auth';
export const authModule = new Elysia({ prefix: '/auth' })
.mount(auth.handler);
App Entry Point
import { Elysia } from 'elysia';
import { openapi } from '@elysiajs/openapi';
import { cors } from '@elysiajs/cors';
import { connectDB } from '@/shared/infrastructure/mongodb';
import { userModule } from './modules/userModule';
import { authModule } from './modules/authModule';
await connectDB();
const app = new Elysia()
.use(cors())
.use(openapi())
.use(authModule)
.use(userModule)
.get('/health', async () => {
const { mongoose } = await import('@/shared/infrastructure/mongodb');
return {
status: 'ok',
mongodb: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected',
};
})
.onError(() => {
.(error);
(error..()) {
set. = ;
{ : error. };
}
(error..() || error..()) {
set. = ;
{ : error. };
}
set. = ;
{ : };
})
.();
.();
= app;
MongoDB Aggregation Examples
Complex Query in Repository
async getUserStats(): Promise<{ totalUsers: number; byRole: Record<string, number> }> {
const result = await UserModel.aggregate([
{
$group: {
_id: '$role',
count: { $sum: 1 },
},
},
]);
const byRole: Record<string, number> = {};
let total = 0;
for (const item of result) {
byRole[item._id] = item.count;
total += item.count;
}
return { totalUsers: total, byRole };
}
async getRecentlyActiveUsers(days: number): Promise<User[]> {
const since = new Date();
since.setDate(since.getDate() - days);
const docs = await UserModel.find({
: { : since },
})
.({ : - })
.()
.();
docs.( .(doc));
}
Testing with Bun
import { describe, it, expect, mock, beforeEach } from 'bun:test';
import { CreateUserCommand } from '../CreateUserCommand';
describe('CreateUserCommand', () => {
let mockRepo: any;
beforeEach(() => {
mockRepo = {
findByEmail: mock(() => Promise.resolve(null)),
save: mock(() => Promise.resolve()),
};
});
it('should create a user with valid input', async () => {
const command = new CreateUserCommand(mockRepo);
const result = await command.execute({
email: 'test@example.com',
name: 'Test User',
password: 'Password123',
});
expect(result.email).toBe('test@example.com');
expect(result.name).toBe();
(result.).();
(mockRepo.).();
});
(, () => {
command = (mockRepo);
(
command.({
: ,
: ,
: ,
})
)..();
});
(, () => {
mockRepo. = ( .({ : }));
command = (mockRepo);
(
command.({
: ,
: ,
: ,
})
)..();
});
});
Common Commands
bun init
bun add elysia @elysiajs/openapi @elysiajs/cors
bun add mongoose better-auth
bun add -d typescript @types/bun
docker run -d -p 27017:27017 --name mongodb mongo:7
bun run --watch src/index.ts
bun test
bun build src/index.ts --outdir dist --target bun
Key MongoDB Best Practices
- Use Indexes - Create indexes for frequently queried fields
- Select Fields - Use
.select() to limit returned fields
- Lean Queries - Use
.lean() for read-only operations (faster)
- Aggregation - Use aggregation pipeline for complex queries
- Transactions - Use sessions for multi-document transactions
- Connection Pooling - Mongoose handles this automatically
- Schema Validation - Use Mongoose validators for data integrity
Document Design Patterns
Embedded Documents (Denormalization)
profile: {
avatar: String,
bio: String,
}
Referenced Documents
const orderSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'User' },
});
const order = await OrderModel.findById(id).populate('user');
Hybrid Approach
const orderSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'User' },
userName: String,
userEmail: String,
});