Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

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:

with
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.

title="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[];

}
title="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:

title=".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.

From the project root, run:

bunx prisma@latest orm init --target postgres
Bash
pnpm dlx prisma@latest orm init --target postgres
Bash
yarn dlx prisma@latest orm init --target postgres
Bash
npx 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:

no-copy
"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.

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.
title="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.

Read the live schema into the contract:

bunx prisma contract infer --output ./src/prisma/contract.prisma
Bash
pnpm prisma contract infer --output ./src/prisma/contract.prisma
Bash
yarn prisma contract infer --output ./src/prisma/contract.prisma
Bash
npx prisma contract infer --output ./src/prisma/contract.prisma
no-copy
"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 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:

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

title="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 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:

title="src/prisma/contract.prisma"
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 emit
Bash
pnpm prisma contract emit
Bash
yarn prisma contract emit
Bash
npx prisma contract emit
no-copy
"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 sign
Bash
pnpm prisma db sign
Bash
yarn prisma db sign
Bash
npx prisma db sign
no-copy
"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 verify
Bash
pnpm prisma db verify
Bash
yarn prisma db verify
Bash
npx prisma db verify
no-copy
"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.

title="TypeORM"
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();
Prisma ORM
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.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.

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.transaction rolls everything back, the same as both ORMs. The sandbox confirmed that a user created before a throw did not exist afterwards.
  • .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 Timestamptz for timestamptz columns. Change them to TimestamptzString before you emit, as in step 3, or every query that reads the column fails with RUNTIME.TEMPORAL_UNAVAILABLE on Node.js.
  • 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.

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."
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu