SQL ORMs
Learn how to migrate from Sequelize or TypeORM to Prisma ORM
Introduction
This guide shows you how to migrate an application from Sequelize or TypeORM to Prisma ORM. Your database already exists and your current ORM already shaped it, so Prisma ORM starts from the live schema: it reads the tables into a contract, records that the database matches it, and lets you replace queries one at a time while the old ORM keeps running.
The example is a small TypeORM app with users and posts. Every command and query below was run against it on a local PostgreSQL database; the Sequelize calls in the query mapping table map onto the same Prisma ORM calls.
Prisma ORM 8 is a release candidate. Prisma ORM 7 remains fully supported; the Prisma ORM 7 version of this guide is at /guides/v7/switch-to-prisma-orm/from-sql-orms.
Prerequisites
- Node.js 22.18 or newer on the 22 line, 24.11 or newer on the 24 line, or 26 or newer (24 recommended)
- A Sequelize or TypeORM project that already connects to a PostgreSQL database
- A TypeScript project. The scripts in this guide are written for an app that runs as ES modules, and steps 2 and 5 say what to change in a CommonJS app
Use with your agent
To delegate this guide to your coding agent, copy the prompt below and hand it over:
1. The starting point
The app has two TypeORM entities. synchronize: true in the data source created the user and post tables in PostgreSQL, and a script inserted two users and a post through TypeORM repositories.
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: "varchar", unique: true })
email!: string;
@Column({ type: "varchar", nullable: true })
name!: string | null;
@Column({ type: "boolean", default: true })
active!: boolean;
@CreateDateColumn({ type: "timestamptz" })
createdAt!: Date;
@OneToMany(() => Post, (post) => post.author)
posts!: Post[];
}@Entity()
export class Post {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: "varchar" })
title!: string;
@Column({ type: "text", nullable: true })
content!: string | null;
@Column({ type: "boolean", default: false })
published!: boolean;
@CreateDateColumn({ type: "timestamptz" })
createdAt!: Date;
@ManyToOne(() => User, (user) => user.posts, { nullable: false, onDelete: "CASCADE" })
author!: User;
}The project reads its connection string from DATABASE_URL:
DATABASE_URL="postgres://user:password@localhost:5432/mydb"Nothing about the database changes in this guide. Prisma ORM adopts the tables as they are, TypeORM keeps working next to it, and both read and write the same rows.
2. Initialize Prisma ORM
From the project root, run:
bunx prisma@latest orm init --target postgresPick PSL as the authoring style and keep the default contract path, src/prisma/contract.prisma. The command adds @prisma/orm-postgres and dotenv to your dependencies, adds prisma and @prisma/cli-engine as dev dependencies, and finishes by emitting the starter contract:
"target": "postgres",
"authoring": "psl",
"schemaPath": "src/prisma/contract.prisma",
"filesWritten": ["src/prisma/contract.prisma", "prisma.config.ts", "src/prisma/db.ts", ".env.example", "tsconfig.json"],
"packagesInstalled": { "status": "installed", "deps": ["@prisma/orm-postgres", "dotenv"], "devDeps": ["prisma", "@prisma/cli-engine"] },
"contractEmitted": trueYou will use these files during the migration:
prisma.config.tsloads.envand points the CLI at the contract andDATABASE_URL. Every command below reads the connection string from there.src/prisma/db.tscreates the client your code imports.src/prisma/contract.prismais the starter contract. The next step overwrites it with your real schema.
import 'dotenv/config';
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL']!,
});There is no prisma generate and no driver adapter in Prisma ORM: the runtime reads contract.json, which contract emit writes, and connects with the url you pass. orm init also changes tsconfig.json: it sets "module": "preserve", "moduleResolution": "bundler", and "resolveJsonModule": true, and it keeps experimentalDecorators and emitDecoratorMetadata in place for TypeORM. In package.json, it adds "type": "module" if the file has no "type" field. If your app is CommonJS, those changes stop it from starting, so follow In a CommonJS project before you run the app again.
3. Infer the contract from your database
Read the live schema into the contract:
bunx prisma contract infer --output ./src/prisma/contract.prisma"summary": "Contract inferred successfully",
"target": { "familyId": "sql", "id": "postgres" },
"psl": { "path": "src/prisma/contract.prisma" }The inferred contract mirrors what TypeORM created, down to the constraint names and the CASCADE on the foreign key. Model names are PascalCase; @@map keeps the physical table names, so no table is renamed:
// use prisma-8
model User {
id Int @id(map: "PK_cace4a159ff9f2512dd42373760") @default(autoincrement())
email VarChar @unique(map: "UQ_e12875dfb3b1d92d7d7c5377e22")
name VarChar?
active Boolean @default(true)
createdAt TimestamptzString @default(now())
posts Post[]
@@map("user")
}
model Post {
id Int @id(map: "PK_be5fda3aac270b134ff9c21cdee") @default(autoincrement())
title VarChar
content String?
published Boolean @default(false)
createdAt TimestamptzString @default(now())
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade, map: "FK_c6fb082a3114f35d0cc27c518e0", index: false)
@@map("post")
}Review the file before you go on. The timestamptz columns come back as TimestamptzString, which reads and writes PostgreSQL's own text form, such as "2026-09-10 21:32:51.298482+06", so your code needs no Temporal. TypeORM and Sequelize gave you a Date there, so convert with new Date(value) where your code needs one.
This is also the moment to delete from contract.prisma the models you do not want Prisma ORM to use yet, or to rename a model while keeping its @@map. The PSL syntax page lists the attributes.
4. Emit and sign
Turn the reviewed contract into the artifacts the runtime and CLI use:
bunx prisma contract emit"files": { "json": "src/prisma/contract.json", "dts": "src/prisma/contract.d.ts" }Then record that the live database matches the contract:
bunx prisma db sign"summary": "Database signed (marker created)",
"target": { "expected": "postgres", "actual": "postgres" },
"marker": { "created": true, "updated": false }db sign verifies the schema first and refuses to sign if the database does not satisfy the contract. It replaces the baseline migration from Prisma ORM 7 (migrate diff plus migrate resolve): the database is not touched, only marked. Check it at any time with:
bunx prisma db verify"summary": "Database marker and schema match contract"If you edit the contract again later, run contract emit and then db sign again; db verify reports Hash mismatch until you do.
5. Replace queries
Rewrite queries one at a time. The TypeORM tab shows the queries the app runs today; the Prisma ORM tab shows the same operations rewritten, in a script that runs against the rows TypeORM created (it uses new emails so both scripts can run against the same database). The sequence covers the calls most apps live on: create, a transaction, find many, find one with a relation, update, and delete.
import { AppDataSource } from "./data-source.js";
import { User } from "./entities/User.js";
import { Post } from "./entities/Post.js";
await AppDataSource.initialize();
const userRepository = AppDataSource.getRepository(User);
const postRepository = AppDataSource.getRepository(Post);
// Create
const alice = userRepository.create({ email: "alice@prisma.io", name: "Alice" });
await userRepository.save(alice);
// Transaction
await AppDataSource.transaction(async (manager) => {
const bob = manager.create(User, { email: "bob@prisma.io", name: "Bob" });
await manager.save(bob);
const post = manager.create(Post, { title: "Hello from TypeORM", author: bob });
await manager.save(post);
});
// Find many
const users = await userRepository.find({
where: { active: true },
take: 10,
order: { createdAt: "DESC" },
});
// Find one with relation
const bobWithPosts = await userRepository.findOne({
where: { email: "bob@prisma.io" },
relations: { posts: true },
});
// Update
await userRepository.update(alice.id, { name: "Alicia" });
// Delete
await userRepository.delete(alice.id);
console.log("count", await userRepository.count());
await AppDataSource.destroy();Run it with Node.js, which executes TypeScript directly:
node src/prisma-queries.tsIn a CommonJS project this command fails, because the script uses top-level await and imports a file by its .ts name. Change the script as follows: import db from "./prisma/db", move every line below the imports into an async function main(), and call main() on the last line. Then run it with npx tsx src/prisma-queries.ts, or compile it with the rest of your app and run the compiled file with node.
created { active: true, createdAt: '2026-09-10 21:57:22.667511+06', email: 'carol@prisma.io', id: 9, name: 'Carol' }
transaction {
dave: { active: true, createdAt: '2026-09-10 21:57:22.67978+06', email: 'dave@prisma.io', id: 10, name: 'Dave' },
post: { authorId: 10, content: null, createdAt: '2026-09-10 21:57:22.67978+06', id: 4, published: false, title: 'Hello from Prisma 8' }
}
users [
{ active: true, createdAt: '2026-09-10 21:57:22.67978+06', email: 'dave@prisma.io', id: 10, name: 'Dave' },
{ active: true, createdAt: '2026-09-10 21:57:22.667511+06', email: 'carol@prisma.io', id: 9, name: 'Carol' },
{ active: true, createdAt: '2026-09-10 21:29:00.614611+06', email: 'bob@prisma.io', id: 2, name: 'Bob' }
]
bobWithPosts {
active: true, createdAt: '2026-09-10 21:29:00.614611+06', email: 'bob@prisma.io', id: 2, name: 'Bob',
posts: [ { authorId: 2, content: null, createdAt: '2026-09-10 21:29:00.614611+06', id: 1, published: false, title: 'Hello from TypeORM' } ]
}
updated { active: true, createdAt: '2026-09-10 21:57:22.667511+06', email: 'carol@prisma.io', id: 9, name: 'Caroline' }
deleted { active: true, createdAt: '2026-09-10 21:57:22.667511+06', email: 'carol@prisma.io', id: 9, name: 'Caroline' }
totals { users: 2 }What changed, in the order the script runs:
- On PostgreSQL you write the schema name, usually
public, betweendb.ormand the model name:db.orm.public.User. You do not use a repository or a model class. There is nosave()step;createinserts and returns the full row, defaults included. db.transactiongives the callback atxwith the sameormmethods. Usetx.orminside it; whatever the callback returns comes out ofdb.transaction.- Filters and sorting chain:
.where({ active: true })for equality, a lambda such as(u) => u.createdAt.desc()for sorting,.limit()fortake, and.all()or.first()to run the query. .include("posts")replacesrelations: { posts: true }. The row Prisma ORM returned for Bob carries the post TypeORM wrote, because both ORMs read the same table..update()and.delete()act on one row and return it. For many rows useupdateAll()anddeleteAll(), orupdateAndCount()anddeleteAndCount()when you only need the number.- A total count is an aggregate:
.aggregate((a) => ({ users: a.count() })). - The script closes the pool with
db.close()because it is a one-off process. A server keeps the client open for its lifetime.
TypeORM still reads everything Prisma ORM wrote; a find({ relations: { posts: true } }) after the script returned Dave and his post alongside Bob. Keep TypeORM for the queries you have not rewritten yet, and remove it when the last one is gone.
6. Query mapping
Every Prisma ORM call in this table was run against the sandbox app above. The Sequelize and TypeORM columns show the call it replaces.
| Operation | Sequelize | TypeORM | Prisma ORM |
|---|---|---|---|
| Find many | User.findAll({ where: { active: true }, order: [["createdAt", "DESC"]], limit: 10 }) | repo.find({ where: { active: true }, order: { createdAt: "DESC" }, take: 10 }) | db.orm.public.User.where({ active: true }).orderBy((u) => u.createdAt.desc()).limit(10).all() |
| Find one | User.findOne({ where: { email } }) | repo.findOne({ where: { email } }) | db.orm.public.User.where({ email }).first() |
| Find by primary key | User.findByPk(id) | repo.findOneBy({ id }) | db.orm.public.User.first({ id }) |
| Pattern filter, selected columns | User.findAll({ where: { email: { [Op.iLike]: "%@prisma.io" } }, attributes: ["id", "email"] }) | repo.find({ where: { email: ILike("%@prisma.io") }, select: { id: true, email: true } }) | db.orm.public.User.where((u) => u.email.ilike("%@prisma.io")).select("id", "email").all() |
| Offset pagination | User.findAll({ offset: 20, limit: 10 }) | repo.find({ skip: 20, take: 10 }) | db.orm.public.User.orderBy((u) => u.id.asc()).offset(20).limit(10).all() |
| Load a relation | User.findAll({ include: [Post] }) | repo.find({ relations: { posts: true } }) | db.orm.public.User.include("posts").all() |
| Load the parent | Post.findAll({ include: [User] }) | postRepo.find({ relations: { author: true } }) | db.orm.public.Post.include("author").all() |
| Shape a relation | include: [{ model: Post, attributes: ["id", "title"], limit: 5 }] | separate query | .include("posts", (p) => p.select("id", "title").orderBy((x) => x.createdAt.desc()).limit(5)) |
| Count a relation | Post.count({ group: ["authorId"] }) | postRepo.countBy({ author: { id } }) per user | db.orm.public.User.include("posts", (p) => p.count()).all() |
| Count rows | User.count() | repo.count() | db.orm.public.User.aggregate((a) => ({ users: a.count() })) |
| Create | User.create({ email, name }) | repo.save(repo.create({ email, name })) | db.orm.public.User.create({ email, name }) |
| Create many | User.bulkCreate([...]) | repo.save([...]) | db.orm.public.User.createAll([...]) |
| Update one | User.update({ name }, { where: { id } }) | repo.update(id, { name }) | db.orm.public.User.where({ id }).update({ name }) |
| Update many | User.update({ active: false }, { where: { email: emails } }) | repo.update({ email: In(emails) }, { active: false }) | db.orm.public.User.where((u) => u.email.in(emails)).updateAll({ active: false }) |
| Update many, count only | const [count] = await User.update(...) | (await repo.update(...)).affected | db.orm.public.User.where({ active: false }).updateAndCount({ active: true }) |
| Delete one | User.destroy({ where: { id } }) | repo.delete(id) | db.orm.public.User.where({ id }).delete() |
| Delete many | User.destroy({ where: { email: emails } }) | repo.delete({ email: In(emails) }) | db.orm.public.User.where((u) => u.email.in(emails)).deleteAll() |
| Transaction | sequelize.transaction(async (t) => { ... { transaction: t } }) | dataSource.transaction(async (manager) => { ... }) | db.transaction(async (tx) => { ... tx.orm.public.User.create(...) }) |
Two differences to keep in mind while you translate:
- Prisma ORM queries return plain objects, not entity instances. There is nothing to
save()after changing a field; call.update()with the fields. - A thrown error inside
db.transactionrolls everything back, the same as both ORMs. The sandbox confirmed that a user created before athrowdid not exist afterwards.
Common gotchas
Turn off schema synchronization in the ORM you are leaving once the database is signed: synchronize: false in the TypeORM data source, and no sequelize.sync() on startup. Prisma ORM only knows the columns in the contract, so a column the old ORM adds behind it is invisible to your Prisma ORM queries until you update the contract and run contract emit again. db verify accepts extra columns, so it will not warn you.
.update()and.delete()change exactly one row, even when the filter matches several. UseupdateAll()ordeleteAll()for bulk changes; this is the opposite default from TypeORM'srepo.update(criteria, ...), which updates every match.contract inferwritesTimestamptzStringfortimestamptzcolumns, so their values are strings, notDateobjects. Convert withnew Date(value)where your code needs aDate.- If
package.jsondeclares"type": "commonjs",orm initkeeps it and prints a warning. If it has no"type"field,orm initadds"type": "module". Either way, a CommonJS app needs the changes in In a CommonJS project. - Do not run
db initordb updateon the existing database.db signis the step for a database that already has the tables;db updateandmigration plancome later, when you change the contract yourself. - Do not call
db.close()in a request handler. The pool is shared across requests; close it only when a script or the process ends.
Prompt your coding agent
Run npx prisma@latest init once to install the Prisma ORM skills for your coding agent and keep them matching your installed packages. Prompts that map to this guide:
- "Using the prisma-8 skill, rewrite every TypeORM read in
src/services/users.tsas a Prisma ORM query and leave the writes on TypeORM." - "Translate the Sequelize
includein the posts list to a Prisma ORM.include()with onlyidandtitleselected." - "Move the signup flow's two inserts into a single
db.transaction."
Next steps
- Reading data and writing data: the full filter, sort, pagination, and mutation surface.
- Relations and joins:
.include()with refinements and relation filters. - How migrations work: once Prisma ORM owns the schema, change the contract and use
db updatein development ormigration planfor checked-in migrations. - Add Prisma ORM to an existing PostgreSQL database: the same
orm init,contract infer,db signflow without the ORM context.
