Prisma ORM 8 is here.Read the docs
Switch to Prisma ORM

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.

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:

Use with your agent
Use with your agent
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 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).

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.

src/entities/User.ts
@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[];
}
src/entities/Post.ts
@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:

.env
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 postgres

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": true

You will use these files during the migration:

  • prisma.config.ts loads .env and points the CLI at the contract and DATABASE_URL. Every command below reads the connection string from there.
  • src/prisma/db.ts creates the client your code imports.
  • src/prisma/contract.prisma is the starter contract. The next step overwrites it with your real schema.
src/prisma/db.ts
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:

src/prisma/contract.prisma
// 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.

src/typeorm-queries.ts
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.ts

In 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, between db.orm and the model name: db.orm.public.User. You do not use a repository or a model class. There is no save() step; create inserts and returns the full row, defaults included.
  • db.transaction gives the callback a tx with the same orm methods. Use tx.orm inside it; whatever the callback returns comes out of db.transaction.
  • Filters and sorting chain: .where({ active: true }) for equality, a lambda such as (u) => u.createdAt.desc() for sorting, .limit() for take, and .all() or .first() to run the query.
  • .include("posts") replaces relations: { 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 use updateAll() and deleteAll(), or updateAndCount() and deleteAndCount() 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.

OperationSequelizeTypeORMPrisma ORM
Find manyUser.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 oneUser.findOne({ where: { email } })repo.findOne({ where: { email } })db.orm.public.User.where({ email }).first()
Find by primary keyUser.findByPk(id)repo.findOneBy({ id })db.orm.public.User.first({ id })
Pattern filter, selected columnsUser.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 paginationUser.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 relationUser.findAll({ include: [Post] })repo.find({ relations: { posts: true } })db.orm.public.User.include("posts").all()
Load the parentPost.findAll({ include: [User] })postRepo.find({ relations: { author: true } })db.orm.public.Post.include("author").all()
Shape a relationinclude: [{ 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 relationPost.count({ group: ["authorId"] })postRepo.countBy({ author: { id } }) per userdb.orm.public.User.include("posts", (p) => p.count()).all()
Count rowsUser.count()repo.count()db.orm.public.User.aggregate((a) => ({ users: a.count() }))
CreateUser.create({ email, name })repo.save(repo.create({ email, name }))db.orm.public.User.create({ email, name })
Create manyUser.bulkCreate([...])repo.save([...])db.orm.public.User.createAll([...])
Update oneUser.update({ name }, { where: { id } })repo.update(id, { name })db.orm.public.User.where({ id }).update({ name })
Update manyUser.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 onlyconst [count] = await User.update(...)(await repo.update(...)).affecteddb.orm.public.User.where({ active: false }).updateAndCount({ active: true })
Delete oneUser.destroy({ where: { id } })repo.delete(id)db.orm.public.User.where({ id }).delete()
Delete manyUser.destroy({ where: { email: emails } })repo.delete({ email: In(emails) })db.orm.public.User.where((u) => u.email.in(emails)).deleteAll()
Transactionsequelize.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.transaction rolls everything back, the same as both ORMs. The sandbox confirmed that a user created before a throw did not exist afterwards.

Common gotchas

  • .update() and .delete() change exactly one row, even when the filter matches several. Use updateAll() or deleteAll() for bulk changes; this is the opposite default from TypeORM's repo.update(criteria, ...), which updates every match.
  • contract infer writes TimestamptzString for timestamptz columns, so their values are strings, not Date objects. Convert with new Date(value) where your code needs a Date.
  • If package.json declares "type": "commonjs", orm init keeps it and prints a warning. If it has no "type" field, orm init adds "type": "module". Either way, a CommonJS app needs the changes in In a CommonJS project.
  • Do not run db init or db update on the existing database. db sign is the step for a database that already has the tables; db update and migration plan come 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.ts as a Prisma ORM query and leave the writes on TypeORM."
  • "Translate the Sequelize include in the posts list to a Prisma ORM .include() with only id and title selected."
  • "Move the signup flow's two inserts into a single db.transaction."

Next steps

On this page