Two ORMs, One Database
When migrating from PHP to TypeScript and starting a new backend, one of the first decisions is which ORM to use: TypeORM, Prisma, Drizzle, MikroORM, Sequelize. Each has advocates who will tell you their choice is clearly superior.
Having used two of these in production simultaneously — TypeORM for a core backend API and Prisma for an admin panel, both connecting to the same MySQL database with 139 entities — I have an opinion on when each is the right choice.
TypeORM: Strengths and Weaknesses
What TypeORM does well:
Decorator-based entities that feel like the domain model:
typescript@Entity()
export class Subscription {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, user => user.subscriptions)
user: User;
@OneToMany(() => Order, order => order.subscription)
orders: Order[];
@Column({ type: 'enum', enum: SubscriptionStatus })
status: SubscriptionStatus;
}The entity definition is the type. No separate schema file or generated types — the class you define is what TypeScript uses everywhere.
Repository pattern: TypeORM's repository abstraction maps well to domain-driven design. Each entity gets a repository; business logic operates through repositories. For a platform with 139 entities, this structure scales.
Relation loading control: Fine-grained control over when related entities are loaded via lazy/eager loading and explicit relations in find options. For subscription queries that conditionally load orders, deliveries, and payments, this matters for performance.
Where TypeORM struggles:
N+1 queries: TypeORM's relation loading can silently generate N+1 queries if you're not explicit about joins. A meaningful source of production performance issues if you're not careful.
Schema migrations: The generated migrations are sometimes suboptimal. Reviewing and occasionally rewriting generated migrations is normal maintenance.
Prisma: Strengths and Weaknesses
What Prisma does well:
Schema introspection — generating types from an existing database:
bashprisma db pullThis inspects the existing database and generates a Prisma schema file with corresponding TypeScript types. For an admin panel connecting to an existing database managed by TypeORM in the backend, this was transformative. We got full type safety across all 139 entities without writing a single entity definition manually.
The fully-typed query builder:
typescriptconst orders = await prisma.order.findMany({
where: {
subscription: { status: 'ACTIVE' },
deliveryDate: { gte: new Date() }
},
include: {
subscription: { include: { user: true } },
payment: true
},
orderBy: { deliveryDate: 'asc' }
});The where, include, and orderBy options are all type-checked — invalid field names or wrong types are compile-time errors.
Where Prisma struggles:
Schema ownership: When the database is managed by a different ORM, the Prisma schema is generated rather than authoritative. Schema updates require regenerating after migrations run.
Complex domain logic: Prisma's flat query model doesn't map as naturally to a domain-rich model as TypeORM's entity classes. For the backend API with complex subscription state transitions, TypeORM is more appropriate.
The Recommendation
Use TypeORM when:
- ·Building the primary application that owns the schema
- ·Significant domain logic with entity lifecycle concerns (
@BeforeInsert,@AfterUpdatehooks) - ·Entities as first-class objects throughout your application code
Use Prisma when:
- ·Building a secondary application connecting to an existing database (
prisma db pullworkflow) - ·Read-heavy admin tooling or reporting
- ·Building a Next.js full-stack application (Prisma has first-class Next.js support)
The honest truth: both are good. The debate generates more controversy than the decision warrants. The differences matter at the edges — complex domain logic, schema ownership, admin tooling. Match the ORM to the use case.