MikroORM
MikroORM is "a TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns" designed as a modern, high-performance ORM. Supporting MongoDB, MySQL, MariaDB, PostgreSQL, SQLite, and more databases, it prioritizes strict type safety and testability. It provides rich features including automatic migrations, entity generators, and query builders, optimized for enterprise development with excellent maintainability and scalability.
GitHub Overview
mikro-orm/mikro-orm
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.
Topics
Star History
Library
MikroORM
Overview
MikroORM is "a TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns" designed as a modern, high-performance ORM. Supporting MongoDB, MySQL, MariaDB, PostgreSQL, SQLite, and more databases, it prioritizes strict type safety and testability. It provides rich features including automatic migrations, entity generators, and query builders, optimized for enterprise development with excellent maintainability and scalability.
Details
MikroORM 2025 edition stands out as an ORM implementation that embodies best practices for modern TypeScript development. By adopting the Data Mapper pattern, it achieves clear separation between domain logic and data access layers, enabling highly maintainable and extensible architecture. Features such as decorator-based entity definitions, data access abstraction through the Repository pattern, and efficient object management via Identity Map make it ideal for large-scale application development. It provides full TypeScript support, comprehensive testing capabilities, and rich relationship management features. The framework emphasizes developer experience with excellent IDE support, automatic code generation, and extensive customization options while maintaining high performance through optimized query generation and caching strategies.
Key Features
- Data Mapper Pattern: Complete separation of domain logic and data access layers
- Full TypeScript Support: Strict type safety and excellent IDE integration
- Multi-Database Support: MongoDB, MySQL, PostgreSQL, SQLite, and more
- Repository Pattern: Unified data access interface
- Automatic Migrations: Automated schema change management
- High Performance: Optimization through Identity Map and Unit of Work
Pros and Cons
Pros
- Excellent development experience and type safety through full TypeScript support
- Loosely coupled, maintainable architecture via Data Mapper pattern
- Comprehensive testing support with high testability
- Rich relationship management and query optimization features
- Automatic migrations and powerful schema management
- Active community with excellent documentation
Cons
- Higher learning curve for developers familiar with Active Record patterns
- Potentially over-complex for small-scale projects
- Advanced knowledge required for performance tuning
- Limited relational ORM features when using MongoDB
- More configuration options compared to other TypeScript ORMs
- Debugging can be challenging due to Data Mapper abstraction during troubleshooting
Reference Pages
Code Examples
Installation and Basic Setup
# Install core packages
npm install @mikro-orm/core @mikro-orm/cli
# Install database-specific drivers
# PostgreSQL
npm install @mikro-orm/postgresql
# MySQL
npm install @mikro-orm/mysql
# SQLite
npm install @mikro-orm/sqlite
# MongoDB
npm install @mikro-orm/mongodb
# Reflection metadata support
npm install reflect-metadata
Entity Definition and Basic Operations
import { Entity, PrimaryKey, Property, ManyToOne, OneToMany, Collection } from '@mikro-orm/core';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Property({ unique: true })
email!: string;
@Property()
createdAt: Date = new Date();
@Property({ onUpdate: () => new Date() })
updatedAt: Date = new Date();
@OneToMany(() => Article, article => article.author)
articles = new Collection<Article>(this);
}
@Entity()
export class Article {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property({ type: 'text' })
content!: string;
@Property()
publishedAt?: Date;
@ManyToOne(() => User)
author!: User;
}
// MikroORM Configuration
import { MikroORM } from '@mikro-orm/core';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
const orm = await MikroORM.init({
entities: [User, Article],
driver: PostgreSqlDriver,
dbName: 'my_database',
user: 'username',
password: 'password',
host: 'localhost',
port: 5432,
debug: true,
});
// Basic CRUD Operations
const em = orm.em.fork();
// Create user
const user = new User();
user.name = 'John Doe';
user.email = '[email protected]';
await em.persist(user).flush();
// Find users
const users = await em.find(User, { name: 'John Doe' });
const userWithArticles = await em.findOneOrFail(User, 1, {
populate: ['articles']
});
Repository Pattern and Service Layer
import { EntityRepository } from '@mikro-orm/core';
import { InjectRepository } from '@mikro-orm/nestjs';
// Custom Repository
export class UserRepository extends EntityRepository<User> {
async findByEmail(email: string): Promise<User | null> {
return this.findOne({ email });
}
async findActiveUsers(): Promise<User[]> {
return this.find({
createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
});
}
async getUserWithRecentArticles(userId: number): Promise<User | null> {
return this.findOne(userId, {
populate: {
articles: {
where: {
publishedAt: { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
},
orderBy: { publishedAt: 'DESC' }
}
}
});
}
}
// Service Layer with Repository Usage
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: UserRepository,
@InjectRepository(Article)
private readonly articleRepository: EntityRepository<Article>
) {}
async createUser(userData: { name: string; email: string }): Promise<User> {
// Duplicate check
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new Error('User already exists');
}
const user = new User();
user.name = userData.name;
user.email = userData.email;
await this.userRepository.getEntityManager().persist(user).flush();
return user;
}
async getUserDashboard(userId: number) {
const user = await this.userRepository.getUserWithRecentArticles(userId);
if (!user) {
throw new Error('User not found');
}
return {
user: {
id: user.id,
name: user.name,
email: user.email
},
recentArticles: user.articles.getItems().map(article => ({
id: article.id,
title: article.title,
publishedAt: article.publishedAt
}))
};
}
}
Advanced Queries and Transaction Management
import { QueryBuilder, LoadStrategy } from '@mikro-orm/core';
// Complex Query Builder Usage
class AdvancedUserService {
constructor(private em: EntityManager) {}
async getPopularAuthors(limit: number = 10) {
const qb = this.em.createQueryBuilder(User, 'u')
.select(['u.*', 'COUNT(a.id) as article_count'])
.leftJoin('u.articles', 'a')
.where('a.publishedAt > ?', [new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)])
.groupBy('u.id')
.orderBy({ article_count: 'DESC' })
.limit(limit);
return await qb.getResult();
}
async searchArticlesWithFacets(searchTerm: string, authorId?: number) {
const qb = this.em.createQueryBuilder(Article, 'a')
.select('a.*')
.leftJoinAndSelect('a.author', 'author')
.where('a.title ILIKE ? OR a.content ILIKE ?',
[`%${searchTerm}%`, `%${searchTerm}%`]);
if (authorId) {
qb.andWhere('a.author = ?', [authorId]);
}
return await qb.getResult();
}
// Transaction Management
async transferArticleOwnership(fromUserId: number, toUserId: number, articleIds: number[]) {
await this.em.transactional(async (em) => {
// Validate sender and receiver
const fromUser = await em.findOneOrFail(User, fromUserId);
const toUser = await em.findOneOrFail(User, toUserId);
// Verify ownership and transfer articles
const articles = await em.find(Article, {
id: { $in: articleIds },
author: fromUser
});
if (articles.length !== articleIds.length) {
throw new Error('Some articles not found or insufficient permissions');
}
// Transfer ownership
articles.forEach(article => {
article.author = toUser;
});
// Audit log
console.log(`Article ownership transferred: ${articleIds.length} articles ${fromUserId} -> ${toUserId}`);
});
}
// Batch Operations
async bulkUpdateArticleStatus(authorId: number, published: boolean) {
const result = await this.em.nativeUpdate(Article,
{ author: authorId },
{ publishedAt: published ? new Date() : null }
);
return { affected: result };
}
}
Migration and Schema Management
// mikro-orm.config.ts
import { defineConfig } from '@mikro-orm/core';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { Migrator } from '@mikro-orm/migrations';
import { EntityGenerator } from '@mikro-orm/entity-generator';
export default defineConfig({
entities: ['./src/entities/**/*.ts'],
entitiesTs: ['./src/entities/**/*.ts'],
driver: PostgreSqlDriver,
dbName: 'my_database',
migrations: {
path: './src/migrations',
pattern: /^[\w-]+\d+\.[jt]s$/,
transactional: true,
allOrNothing: true,
},
extensions: [Migrator, EntityGenerator],
debug: process.env.NODE_ENV !== 'production',
});
// CLI Command Examples
// Create migration
// npx mikro-orm migration:create
// Run migrations
// npx mikro-orm migration:up
// Update schema
// npx mikro-orm schema:update --run
// Generate entities from existing database
// npx mikro-orm generate-entities
Testing Environment Usage
import { MikroORM } from '@mikro-orm/core';
import { SqliteDriver } from '@mikro-orm/sqlite';
// Test Configuration
export async function createTestORM() {
const orm = await MikroORM.init({
entities: [User, Article],
driver: SqliteDriver,
dbName: ':memory:', // In-memory database
allowGlobalContext: true,
});
await orm.schema.createSchema();
return orm;
}
// Jest Test with ORM
describe('UserService', () => {
let orm: MikroORM;
let userService: UserService;
beforeAll(async () => {
orm = await createTestORM();
userService = new UserService(orm.em.getRepository(User), orm.em.getRepository(Article));
});
beforeEach(async () => {
await orm.schema.clearDatabase();
});
afterAll(async () => {
await orm.close();
});
test('should create user successfully', async () => {
const userData = { name: 'Test User', email: '[email protected]' };
const user = await userService.createUser(userData);
expect(user.id).toBeDefined();
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
});
test('should throw error for duplicate email', async () => {
const userData = { name: 'User1', email: '[email protected]' };
await userService.createUser(userData);
await expect(userService.createUser(userData))
.rejects.toThrow('User already exists');
});
});
Performance Optimization and Best Practices
// Avoiding N+1 Problems and Optimization
class OptimizedQueries {
constructor(private em: EntityManager) {}
// Bad example: N+1 problem
async getBadUserArticles() {
const users = await this.em.find(User, {});
const results = [];
for (const user of users) {
const articles = await this.em.find(Article, { author: user }); // N+1!
results.push({ user, articles });
}
return results;
}
// Good example: Proper populate usage
async getGoodUserArticles() {
return await this.em.find(User, {}, {
populate: ['articles'],
strategy: LoadStrategy.JOINED // Use JOIN queries
});
}
// Batch Loading
async getUsersWithArticleCount() {
const qb = this.em.createQueryBuilder(User, 'u')
.select(['u.*', 'COUNT(a.id) as article_count'])
.leftJoin('u.articles', 'a')
.groupBy('u.id');
return await qb.getResultAndCount();
}
// Partial Loading
async getUserSummaries() {
return await this.em.find(User, {}, {
fields: ['id', 'name', 'email'], // Only necessary fields
});
}
// Caching Strategy
private cache = new Map<string, any>();
async getCachedPopularArticles(ttl: number = 300000) { // 5 minutes
const cacheKey = 'popular_articles';
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const data = await this.em.find(Article,
{ publishedAt: { $ne: null } },
{
orderBy: { publishedAt: 'DESC' },
limit: 20,
populate: ['author']
}
);
this.cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
}
}