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.

ORMTypeScriptNode.jsData MapperRepository PatternUnit Testing

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.

Stars8,427
Watchers49
Forks587
Created:March 15, 2018
Language:TypeScript
License:MIT License

Topics

databasedatamapperentitiesentityidentity-mapjavascriptlibsqlmongodbmysqlnodejsormpostgrepostgresqlsqlsql-serversqlitesqlite3typescripttypescript-ormunit-of-work

Star History

mikro-orm/mikro-orm Star History
Data as of: 8/13/2025, 01:43 AM

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;
  }
}