SQL ORMs
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.
- Node.js 24 or later
- 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
To delegate this guide to your coding agent, copy the prompt below and hand it over:
Add Prisma ORM to this Sequelize or TypeORM project so I can migrate queries incrementally.
1. From the project root, run `npx prisma@latest orm init --yes --target postgres --authoring psl`, then `npx prisma@latest init` so the Prisma agent skills are installed, and use them. Before you run `orm init`, note whether package.json has "type": "module". If it does not, and the code uses `require` or compiles to `require` calls, the app is CommonJS: after `orm init`, apply the changes under "In a CommonJS project" in https://www.prisma.io/docs/cli/orm-init.md. Make sure `.env` contains the DATABASE_URL the app already uses.
2. Run `npx prisma contract infer --output ./src/prisma/contract.prisma` to read the existing tables into a contract. Open the file, change every `Timestamptz` field type to `TimestamptzString`, and keep the `@@map` attributes so model names stay clean while table names stay as they are.
3. Run `npx prisma contract emit`, then `npx prisma db sign`, then `npx prisma db verify`. Do not run `db init` or `db update`; the tables already exist.
4. Following https://www.prisma.io/docs/guides/switch-to-prisma-orm/from-sql-orms.md, rewrite the read queries first, then the writes, importing `db` from `src/prisma/db.ts`. Run each rewritten query with `node <script>.ts` and show me the output. In a CommonJS project, import `db` from `./prisma/db` without the `.ts` extension, put the `await` calls inside an `async` function, and run the script with `npx tsx <script>.ts` instead. Leave the old ORM in place for queries you have not rewritten yet, but turn off its schema sync (`synchronize` in TypeORM, `sequelize.sync()` in Sequelize).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.
From the project root, run:
bunx prisma@latest orm init --target postgrespnpm dlx prisma@latest orm init --target postgresyarn dlx prisma@latest orm init --target postgresnpx 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.
Pick 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.
Read the live schema into the contract:
bunx prisma contract infer --output ./src/prisma/contract.prismapnpm prisma contract infer --output ./src/prisma/contract.prismayarn prisma contract infer --output ./src/prisma/contract.prismanpx 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 Timestamptz @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 Timestamptz @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. One edit is required for this schema: the timestamptz columns come back as Timestamptz, which reads each value as a Temporal value. On Node.js 24 that fails with RUNTIME.TEMPORAL_UNAVAILABLE, because Node.js 24 has no global Temporal. Change them to TimestamptzString, which reads and writes PostgreSQL's own text form and maps to the same column type:
model User { createdAt Timestamptz @default(now()) createdAt TimestamptzString @default(now())}model Post { createdAt Timestamptz @default(now()) createdAt TimestamptzString @default(now())}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.
"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 Timestamptz @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 Timestamptz @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. One edit is required for this schema: the timestamptz columns come back as Timestamptz, which reads each value as a Temporal value. On Node.js 24 that fails with RUNTIME.TEMPORAL_UNAVAILABLE, because Node.js 24 has no global Temporal. Change them to TimestamptzString, which reads and writes PostgreSQL's own text form and maps to the same column type:
model User {
createdAt Timestamptz @default(now())
createdAt TimestamptzString @default(now())
}
model Post {
createdAt Timestamptz @default(now())
createdAt TimestamptzString @default(now())
}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.
Turn the reviewed contract into the artifacts the runtime and CLI use:
bunx prisma contract emitpnpm prisma contract emityarn prisma contract emitnpx prisma contract emit"files": { "json": "src/prisma/contract.json", "dts": "src/prisma/contract.d.ts" }Then record that the live database matches the contract:
"files": { "json": "src/prisma/contract.json", "dts": "src/prisma/contract.d.ts" }Then record that the live database matches the contract:
bunx prisma db signpnpm prisma db signyarn prisma db signnpx 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:
"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 verifypnpm prisma db verifyyarn prisma db verifynpx 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.
"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.
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();import { db } from "./prisma/db.ts";
// Create
const carol = await db.orm.public.User.create({ email: "carol@prisma.io", name: "Carol" });
console.log("created", carol);
// Transaction
const result = await db.transaction(async (tx) => {
const dave = await tx.orm.public.User.create({ email: "dave@prisma.io", name: "Dave" });
const post = await tx.orm.public.Post.create({ title: "Hello from Prisma 8", authorId: dave.id });
return { dave, post };
});
console.log("transaction", result);
// Find many
const users = await db.orm.public.User
.where({ active: true })
.orderBy((u) => u.createdAt.desc())
.limit(10)
.all();
console.log("users", users);
// Find one with relation
const bobWithPosts = await db.orm.public.User
.where({ email: "bob@prisma.io" })
.include("posts")
.first();
console.log("bobWithPosts", bobWithPosts);
// Update
const updated = await db.orm.public.User.where({ id: carol.id }).update({ name: "Caroline" });
console.log("updated", updated);
// Delete
const deleted = await db.orm.public.User.where({ id: carol.id }).delete();
console.log("deleted", deleted);
const totals = await db.orm.public.User.aggregate((a) => ({ users: a.count() }));
console.log("totals", totals);
await db.close();Run it with Node.js 24, 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.
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.
.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 inferwritesTimestamptzfortimestamptzcolumns. Change them toTimestamptzStringbefore you emit, as in step 3, or every query that reads the column fails withRUNTIME.TEMPORAL_UNAVAILABLEon Node.js.- 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.
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."
- 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 project: the same
orm init,contract infer,db signflow without the ORM context.