ORM Basics (Prisma/TypeORM/Sequelize) Cheat Sheet
Compares core ORM concepts—models, migrations, relations, and querying—across Prisma, TypeORM, and Sequelize for Node.js applications.
Prisma Schema
Declarative model definition in schema.prisma.
// schema.prismamodel User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] createdAt DateTime @default(now())}model Post { id Int @id @default(autoincrement()) title String authorId Int author User @relation(fields: [authorId], references: [id])}
TypeORM Entity
Decorator-based entity classes with a relation.
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne } from 'typeorm';@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column({ unique: true }) email: string; @OneToMany(() => Post, (post) => post.author) posts: Post[];}@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @ManyToOne(() => User, (user) => user.posts) author: User;}
Sequelize Model
Defining models and associations, then eager loading.
const { DataTypes } = require('sequelize');const User = sequelize.define('User', { email: { type: DataTypes.STRING, unique: true, allowNull: false }, name: DataTypes.STRING,});const Post = sequelize.define('Post', { title: { type: DataTypes.STRING, allowNull: false },});User.hasMany(Post, { foreignKey: 'authorId' });Post.belongsTo(User, { foreignKey: 'authorId' });// Query with eager loadingconst users = await User.findAll({ include: Post });
Feature Comparison
How the three ORMs differ in approach.
- Prisma- Schema-first with a generated type-safe client; migrations via `prisma migrate`; no active-record pattern, queries go through the Prisma Client
- TypeORM- Decorator-based entity classes; supports both Active Record and Data Mapper patterns; migrations via CLI or auto-sync (dev only)
- Sequelize- JavaScript-first (with optional TypeScript typings) model definitions; mature ecosystem, migrations via `sequelize-cli`
- Migrations- All three support versioned migration files; Prisma also supports `db push` for rapid prototyping without migration history
- Type safety- Prisma generates fully typed query results from your schema automatically; TypeORM/Sequelize rely more on manually maintained types
- Raw SQL escape hatch- All three let you drop to raw SQL (`prisma.$queryRaw`, `query()` in TypeORM, `sequelize.query()`) for cases the query builder can't express
Prisma Nested Writes & Upserts
Create/connect related records and upsert in a single call.
// Connect-or-create a related record atomicallyconst post = await prisma.post.create({ data: { title: 'New Post', author: { connectOrCreate: { where: { email: '[email protected]' }, create: { email: '[email protected]', name: 'Ada' }, }, }, tags: { connect: [{ id: 1 }, { id: 2 }] }, },});// Upsert: update if exists, insert otherwiseawait prisma.user.upsert({ where: { email: '[email protected]' }, update: { name: 'Ada Lovelace' }, create: { email: '[email protected]', name: 'Ada Lovelace' },});
TypeORM QueryBuilder & Repository
Escape the entity manager for complex joins and raw expressions.
const repo = dataSource.getRepository(User);// QueryBuilder for joins the repository API can't express cleanlyconst users = await repo .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('post.published = :pub', { pub: true }) .andWhere('user.createdAt > :since', { since: new Date('2025-01-01') }) .orderBy('user.id', 'DESC') .take(20) .getMany();// Wrap multiple writes in a transactionawait dataSource.transaction(async (manager) => { const user = await manager.save(User, { email: '[email protected]' }); await manager.save(Post, { title: 'Draft', author: user });});
Sequelize Transactions & Hooks
Managed transactions and lifecycle hooks for cross-cutting logic.
// Managed transaction: auto commit/rollback based on promise resolutionawait sequelize.transaction(async (t) => { const user = await User.create({ email: '[email protected]' }, { transaction: t }); await Post.create({ title: 'Draft', authorId: user.id }, { transaction: t });});// Model hooks run around lifecycle eventsUser.beforeCreate((user) => { user.email = user.email.toLowerCase();});User.afterDestroy(async (user) => { await Post.destroy({ where: { authorId: user.id } });});
Advanced Pitfalls & Idioms
Gotchas that only surface once you move past basic CRUD.
- Connection pool exhaustion- Serverless deployments (Lambda, edge functions) can exhaust DB connections fast; use Prisma Data Proxy/Accelerate, TypeORM's pool size tuning, or PgBouncer in front of Postgres
- Soft deletes- Prisma has no built-in soft delete (use a `deletedAt` field + middleware/extension); TypeORM supports `@DeleteDateColumn()` natively; Sequelize offers `paranoid: true`
- Optimistic concurrency- Guard concurrent updates with a `version` column and a `WHERE version = ?` check in the update, since none of the three ORMs enforce this by default
- Cascading deletes- Must be declared explicitly at the schema/entity level (`onDelete: Cascade` in Prisma, `{ onDelete: 'CASCADE' }` in TypeORM relations, `onDelete: 'CASCADE'` in Sequelize associations) or the DB will reject the delete
- Bulk operations bypass hooks- Sequelize's `bulkCreate`/`update` skip individual instance hooks unless `individualHooks: true` is set; Prisma's `createMany`/`updateMany` never run middleware at all
- Migration drift- Manually editing a production schema outside the migration tool causes drift that `prisma migrate dev`, TypeORM's `migration:generate`, and `sequelize-cli` will all silently mis-detect on the next run
Don't let an ORM hide N+1 queries — always check the generated SQL (Prisma's query logging, TypeORM's logging: true, Sequelize's logging: console.log) for any loop that triggers a query per iteration, and use eager loading (include) to fix it.