
← 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.

ByGal Tetinger
Senior Backend Engineer

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

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":
Why Not Prisma?
Prisma's developer experience is excellent, but our POC revealed issues:
Why TypeORM Won
TypeORM hit the sweet spot:
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:
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
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 dataWhen 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.