From Sequelize to TypeORM: A Performance-Driven Migration
← Back
29.3.26

From Sequelize to TypeORM: A Performance-Driven Migration

How we boosted query performance by 60% and achieved full type safety: The story of our migration from Sequelize to TypeORM.

Gal Tetinger
ByGal Tetinger
Senior Backend Engineer

Introduction

Sequelize has been a staple ORM in the Node.js ecosystem for years. It's battle-tested, widely
used, and... showing its age. When our team at Papaya started experiencing performance
bottlenecks and struggled with TypeScript integration, we knew it was time for a change.
This is the story of our migration from Sequelize to TypeORM-the challenges we faced, the
approach we took, and the performance gains that made it all worthwhile.

Why We Needed to Move

The Sequelize Problem

1. Loose Typing
Sequelize served us well for years, but several pain points had accumulated:
Sequelize was built in the JavaScript era, and its TypeScript support feels bolted on. Type safety is achieved through strings, not actual types:

1 // Sequelize: Types through strings - no compile-time safety
2 const user = await User.findOne({
3   where: { iddddd: "123" } // Typo compiles fine!
4 });

This typo would compile successfully and only fail at runtime. We've all been bitten by this.
‍

2. Upgrade Complexity
We attempted to upgrade Sequelize multiple times. Each attempt revealed deep compatibility issues with our codebase. Eventually, we gave up on upgrading entirely-the risk was too high, and the effort too great.

3. Performance Overhead
Sequelize creates full model instances for every row returned from a query. For simple read operations, this object instantiation adds significant CPU overhead. When you're returning thousands of rows, it adds up.

Evaluating Alternatives

The Node.js ORM landscape has evolved. We evaluated the "big three":

ORM TypeScript Support Performance Maturity
Sequelize Retrofitted Heavy instances Very mature
Prisma Excellent Auto-magic queries Modern
TypeORM Native Raw results Mature

Why Not Prisma?

Prisma's developer experience is excellent, but our POC revealed issues:

  • Too much "magic" happening behind the scenes
  • Query behavior was sometimes unpredictable
  • Less control over the generated SQL

Why TypeORM Won

TypeORM hit the sweet spot:

  • Built for TypeScript: True type safety, not string-based
  • Transparent: You understand what queries are being generated
  • Performant: Returns raw results without heavy object instantiation
  • Familiar patterns: Decorators and repositories feel natural

TypeORM: The Basics

Entities Replace Models

In Sequelize, models are defined with string-based field mappings. In TypeORM, entities are proper TypeScript classes:

1 // TypeORM Entity - Real TypeScript types
2 @Entity("users")
3 export class UserEntity {
4   @PrimaryGeneratedColumn()
5   id: number;
6
7   @Column()
8   firstName: string;
9
10   @Column()
11   lastName: string;
12
13   @Column({ type: "timestamp" })
14   createdAt: Date;
15
16   @ManyToOne(() => TeamEntity)
17   team: TeamEntity;
18 }

Now when you query:

1 // TypeORM: Compile-time type safety
2 const user = await userRepository.findOne({
3   where: { firstNameee: "John" } // Compiler error!
4 });

The typo is caught at compile time. No more runtime surprises.

The Repository Pattern
TypeORM uses repositories-type-safe objects bound to specific entities:

1 // Initialize the data source
2 const dataSource = new DataSource({
3   type: "mysql",
4   host: config.DB_HOST,
5   database: config.DB_NAME,
6   entities: [UserEntity, TeamEntity],
7   // MySQL2 options go in 'extra'
8   extra: {
9    connectionLimit: 10,
10   idleTimeout: 60000
11   }
12 });
13
14 await dataSource.initialize();
15
16 // Get a typed repository
17 const userRepository = dataSource.getRepository(UserEntity);
18
19 // All operations are type-safe
20 const user = new UserEntity();
21 user.firstName = "John";
22 user.lastName = "Doe";
23 await userRepository.save(user);

Query Builder for Complex Queries

Sequelize struggles with complex queries, often requiring raw SQL. TypeORM's query builder handles complexity while maintaining type safety:

1 // Complex query with joins, conditions, and ordering
2 const results = await userRepository
3   .createQueryBuilder("user")
4   .leftJoinAndSelect("user.team", "team")
5   .where("user.createdAt > :date", { date: lastMonth })
6   .andWhere("team.isActive = :active", { active: true })
7   .orderBy("user.lastName", "ASC")
8   .take(100)
9   .getMany();

This approach also protects against SQL injection-parameters are properly escaped.

Our Migration Strategy

The Challenge
We couldn't flip a switch and migrate everything at once. With dozens of models and millions of queries per minute, we needed a safer approach.

The Solution: Sampling-Based Rollout
We built a routing layer that could direct queries to either Sequelize or TypeORM based on a sampling percentage:

1 ┌─────────────────┐
2 │  Query Request  │
3 └────────┬────────┘
4          │
5          ▼
6 ┌─────────────────┐
7 │   Feature Flag  │
8 │    (Sampling)   │
9 └────────┬────────┘
10         │
11    ┌────┴───────┐
12    │            │
13    ▼            ▼
14 ┌─────────┐ ┌───────┐
15 │Sequelize│ │TypeORM│
16 └────┬────┘ └───┬───┘
17      │          │
18      └────┬─────┘
19           │
20           ▼
21      ┌─────────┐
22      │ Results │
23      └─────────┘

The Rollout Process

Phase 1: Pick a Simple Service

We started with our user-segments service-relatively isolated with straightforward queries.

Phase 2: Query-by-Query Migration

Rather than migrating the entire service at once, we migrated one query type at a time:

1. Write the TypeORM equivalent
2. Start at 1% sampling
3. Monitor for errors and result differences
4. Gradually increase to 100%

Phase 3: Fix Edge Cases

We discovered minor issues:

  • createdAt and updatedAt weren't auto-populated (TypeORM requires explicit configuration)
  • Some date format differences between the ORMs
  • Transaction handling worked slightly differently

Phase 4: Expand to More Models

After user-segments, we moved to user-activity-records, then began work on the core user
model.

Results: The Numbers

The performance improvements exceeded our expectations.

Query Performance

Service Sequelize Avg TypeORM Avg Improvement
User Segments Baseline -60% 60%+ faster
User Models Baseline -55% 55%+ faster


Why Such a Big Improvement?

The main factor: object instantiation overhead.
Sequelize creates a full model instance for every row:

1 // Sequelize (simplified internal behavior)
2 for (const row of rawResults) {
3   const instance = new Model();
4   instance._setDataValues(row);
5   instance.isNewRecord = false;
6   // ... many more operations
7   results.push(instance);
8 }

TypeORM, by default, returns plain objects:

1 // TypeORM
2 const results = rawResults; // Just the data

When you're processing thousands of rows, eliminating this overhead makes a real difference.

Lessons Learned

1. Sampling Is Essential
Never trust a big-bang migration. Our sampling approach caught several edge cases that would have caused production incidents.
‍
2. Watch for Implicit Behaviors
Sequelize has many implicit behaviors (auto-timestamps, default values) that TypeORM requires explicit configuration for. Document these as you find them.
‍
3. Performance Wins Compound
A 60% improvement in database query time has downstream effects: 
Lower response latency 
Reduced CPU usage 
Better resource utilization under load
‍
4. Team Buy-In Matters 
Migration fatigue is real. We chose to handle the bulk of the migration centrally rather than distributing it across teams-ensured consistent progress.

Should You Migrate? 

Consider migrating from Sequelize to TypeORM if: 

✅ You're using TypeScript and want real type safety 

✅ You're seeing CPU overhead from Sequelize's object instantiation 

✅ You've struggled with Sequelize upgrades 

✅ You need better query builder support for complex queries 

Stick with Sequelize if: 

❌ Your team is deeply familiar with Sequelize patterns 

❌ You're not experiencing performance issues 

❌ Migration effort outweighs expected benefits 

Conclusion 

Migrating ORMs in a production system isn't glamorous work. It's careful, methodical, and requires patience. But the payoff-better type safety, improved performance, and a more maintainable codebase-makes it worthwhile. 

Sequelize served us well for years. TypeORM will serve us better for the years ahead. 

Gal Tetinger is a Software Engineer on the Developer Infrastructure team at Papaya, focusing on platform reliability and developer experience.