Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Prisma ORM 6 to 8 (MongoDB)

This guide is for developers moving a Prisma ORM 6 MongoDB project to Prisma ORM 8. Prisma ORM 7 has no MongoDB connector, so Prisma ORM 8 is the successor path. This is a code and workflow migration against the same database: your data never moves.

  • Node.js 22.18 or newer (on the 24 line, 24.11 or newer); Node.js 24 recommended
  • TypeScript 5.9+
  • MongoDB 8.0+
  • The mongodb node driver 7.x as a peer dependency
  • A replica set if your app uses transactions. The ORM API in Prisma ORM 8 doesn't wrap $transaction yet, so multi-document atomicity goes through the driver's sessions (see step 3d), which MongoDB only allows on a replica set.

Scaffold the project:

npx prisma@latest orm init --yes --target mongodb --authoring psl

init writes a prisma.config.ts at the repo root. You select MongoDB by importing the @prisma/orm-mongo facade instead of setting a provider string in the schema. Point the connection at the same database your v6 app uses.

title="After"
// prisma.config.ts

import 'dotenv/config';

import { definePrismaConfig } from 'prisma/config';

import { defineConfig as ormConfig } from '@prisma/orm-mongo/config';

export default definePrismaConfig({

  orm: ormConfig({

    contract: './src/prisma/contract.prisma',

    db: {

      connection: process.env['DATABASE_URL']!,

    },

  }),

});
Before
// schema.prisma
datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

// Rest of schema...

Run npx prisma contract emit to generate contract.json from your .prisma contract file. Re-run this whenever you change the contract.

Instantiate the Prisma client from the emitted contract. Replace my-app-db with the database name your v6 connection string uses. If the connection URL already carries the database name in its path, omit dbName and the client reads it from the URL:

title="After"
// src/prisma/db.ts

import mongo from '@prisma/orm-mongo/runtime';

import type { Contract } from './contract.d';

import contractJson from './contract.json' with { type: 'json' };

const db = mongo<Contract>({

  contractJson,

  url: process.env['DATABASE_URL']!,

  dbName: 'my-app-db',

});

export { db };
Before
// src/prisma/db.ts
import { PrismaClient } from '../generated/prisma/client';

const prisma = new PrismaClient();

export { prisma };

This step ports the v6 schema.prisma to a contract, authored in PSL (shown here) or TypeScript. The syntax is close to v6, with a few differences.

To start without porting, Prisma ORM 8 can also read the v6 schema.prisma directly: set contract: prisma6Schema("prisma/schema.prisma") in prisma.config.ts, as Use a Prisma ORM 6 MongoDB schema shows. Port the schema when you want to use what the v6 syntax cannot express, such as polymorphism.

In the table below, the left column is how you wrote it in v6 and the right column is the Prisma ORM 8 equivalent:

Concept v6 (schema.prisma) Prisma ORM 8 (contract.prisma)
First line (nothing) // use prisma-8. Without it, contract emit fails with CONTRACT.SOURCE_LOAD_FAILED
Datasource datasource db { provider = "mongodb" } no provider (configured in prisma.config.ts)
Id field id String @id @default(auto()) @map("_id") @db.ObjectId id ObjectId @id @map("_id")
Scalar types Int, Float, Boolean, DateTime Int32, Double, Bool, Date. The v6 names still work, with a PSL_DEPRECATED_SCALAR_NAME warning, and a later release removes them
More scalar types BigInt, Decimal, Bytes, Json Int64, Decimal128, Binary, Json. A Json field holds only JSON values; type a field that holds other BSON values, such as a Date, as Bson
Timestamps @default(now()), @updatedAt createdAt temporal.createdAt(), updatedAt temporal.updatedAt()
Embedded document type Address { ... } type Address { ... } (unchanged)
Index @@index(...), synced by db push @@index(...) / @@unique(...), applied by migrations
Polymorphism not supported @@discriminator(field) on the base + @@base(Base, "value") on each variant.
See Base models and variants

A fuller contract putting together enum, embedded type, relation, and polymorphism:

title="src/prisma/contract.prisma"
// use prisma-8

enum UserRole {

  @@type("mongo/string@1")

  Admin  = "admin"

  Author = "author"

  Reader = "reader"

}

type Address {

  street  String

  city    String

  country String

}

model User {

  id      ObjectId @id @map("_id")

  email   String

  role    UserRole

  address Address?

  posts   Post[]

  @@map("users")

}

model Post {

  id        ObjectId @id @map("_id")

  title     String

  kind      String

  authorId  ObjectId

  createdAt Date

  author    User @relation(fields: [authorId], references: [id])

  @@discriminator(kind)

  @@index([createdAt(sort: Desc), authorId])

  @@map("posts")

}

model Article {

  summary String

  @@base(Post, "article")

}

When you use @@discriminator, declare a variant for every value the field takes in your existing data: the generated validator only accepts declared values, so writes to documents with an undeclared value fail once the validators are live. A variant model must declare at least one field (an optional one is enough).

Many method names look similar, but they do not behave the same way. This section breaks down each category of operation.

Prisma ORM 8 uses a chained API instead of v6's single options object: you start from a collection (like db.orm.users), add filters and options step by step, then finish with a method that runs the query:

title="After"
import { db } from './db';

// Find one

await db.orm.users.where({ email: 'alice@example.com' }).first();

// Create

const user = await db.orm.users.create({ email: 'a@e.com', role: 'author', address: null });

// Update one

await db.orm.users.where({ _id: user._id }).update({ role: 'author' });

// Update many

await db.orm.users.where({ role: 'reader' }).updateAll({ role: 'author' });

// Delete one

await db.orm.users.where({ _id: user._id }).delete();
Before
import { prisma } from './db';

// Find one
await prisma.user.findFirst({ where: { email } });

// Create
const user = await prisma.user.create({ data: { email, role: 'author' } });

// Update one
await prisma.user.update({ where: { id }, data: { role: 'author' } });

// Update many
await prisma.user.updateMany({ where: { role: 'reader' }, data: { role: 'author' } });

// Delete one
await prisma.user.delete({ where: { id } });

Common mistakes:

Don't Do Why
db.orm.User db.orm.users address by storage name, not model name
.where({ email: { equals: 'x' } }) .where({ email: 'x' }) .where(...) takes a plain equality object
.update({ ... }) with no .where(...) .where(...).update({ ... }) every mutation except create needs a .where(...) first
role: 'Author' role: 'author' enums read and write their storage value, not the key name
title="After"
import { db } from './db';

// Eager-load a relation

const withAuthor = await db.orm.posts.include('author').all();

// Query one variant of a polymorphic collection

const articles = await db.orm.posts.variant('Article').all();
Before
import { prisma } from './db';

// Eager-load a relation
const withAuthor = await prisma.post.findMany({ include: { author: true } });

// Query one variant of a polymorphic collection
const articles = await prisma.post.findMany({ where: { kind: 'article' } });

v6's groupBy / aggregate become a typed pipeline: build a plan with db.query, then run it with (await db.runtime()).query. acc provides the group totals (acc.count(), acc.max(...), and friends), the equivalent of v6's _count / _sum / _max.

import { acc } from '@prisma/orm-mongo/query-builder';

const plan = db.query

  .from('posts')

  .group((f) => ({ _id: f.kind, postCount: acc.count(), latest: acc.max(f.createdAt) }))

  .sort({ postCount: -1 })

  .build();

const byKind = await (await db.runtime()).query(plan).toArray();
TypeScript
await prisma.post.groupBy({
  by: ['kind'],
  _count: { _all: true },
  _max: { createdAt: true },
  orderBy: { _count: { kind: 'desc' } },
});

[!WARNING] ObjectId filters need a raw command

Don't add .match((f) => f.authorId.eq(authorId)) to filter by an ObjectId field (_id, or a foreign key like authorId): the typed builder sends the id through as a plain string, so the stage silently matches nothing. Filter ObjectId fields with the ORM client instead (db.orm.posts.where({ authorId }) encodes them for you), or run the aggregation as a raw command carrying a real ObjectId:

TypeScript
import  from '@prisma/orm-mongo/query-ast/execution';
import  from 'mongodb';

const plan = db.query.rawCommand(
  new RawAggregateCommand('posts', [
    { $match:  },
    { $group: { _id: '$kind', postCount: { $count:  }, latest:  } },
    { $sort:  },
  ]),
);
const byKind = await (await db.runtime()).query(plan).toArray();

Prisma ORM 8 has no transaction method for MongoDB yet. For multi-document transactions, use the mongodb driver's Transaction API.

title="After"
import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env['DATABASE_URL']!);

const session = client.startSession();

const mdb = client.db('my-app-db');

try {

  await session.withTransaction(async () => {

    // Pass { session } to EVERY operation: an op without it silently

    // runs outside the transaction.

    await mdb

      .collection('users')

      .insertOne({ email: 'a@e.com', role: 'author' }, { session });

    await mdb

      .collection('posts')

      .updateOne({ _id: postId }, { $set: { authorId } }, { session });

  });

} finally {

  await session.endSession();

}
Before
await prisma.$transaction(async (tx) => {
  // ...writes...
});

The direct replacement for v6's findRaw and aggregateRaw is db.raw.

title="After"
import { db } from './db';

// Raw aggregation on a collection, replaces v6's aggregateRaw / findRaw.

const plan = db.raw

  .collection('posts')

  .aggregate([ ... ])

  .build();

const rows = await (await db.runtime()).query(plan).toArray();
Before
import { prisma } from './db';

// Raw aggregation on a collection.
const rows = await prisma.post.aggregateRaw({
  pipeline: [ ... ],
});

db.raw is collection-scoped, so v6's database-level $runCommandRaw has no ORM equivalent. Construct your own MongoClient and pass it to the binding from step 1 in place of url: the same client then powers the typed ORM and runs arbitrary commands, sharing one connection pool.

// src/prisma/db.ts

import mongo from '@prisma/orm-mongo/runtime';

import { MongoClient } from 'mongodb';

import type { Contract } from './contract.d';

import contractJson from './contract.json' with { type: 'json' };

export const mongoClient = new MongoClient(process.env['DATABASE_URL']!);

export const db = mongo<Contract>({ contractJson, mongoClient, dbName: 'my-app-db' });

// Elsewhere: typed ORM and raw commands from the same client.

await db.orm.users.where({ email: 'alice@example.com' }).first();

await mongoClient.db('my-app-db').command({ ping: 1 });
TypeScript
import { prisma } from './db';

await prisma.$runCommandRaw({ ping: 1 });

For raw reads and writes scoped to a single collection (anything the typed builder can't express), you don't need the driver: db.query.rawCommand(...) packages a raw command into a plan, as shown in 3c. See Raw queries for the full surface.

For raw reads and writes scoped to a single collection (anything the typed builder can't express), you don't need the driver: db.query.rawCommand(...) packages a raw command into a plan, as shown in 3c. See Raw queries for the full surface.

v6 Prisma ORM 8
prisma.user.findMany(...) db.orm.users.where({ ... }).all()
prisma.user.findFirst(...) db.orm.users.where({ ... }).first()
create / update / delete / upsert same names on db.orm.<collection>
updateMany / deleteMany updateAll / deleteAll
aggregate / groupBy db.query.from(...).group(...).build() → (await db.runtime()).query(plan)
findRaw / aggregateRaw db.raw.collection(...).aggregate(...).build() → (await db.runtime()).query(plan), or db.query.rawCommand(...)
$runCommandRaw share a MongoClient with the binding, then mongoClient.db(...).command(...) (see 3e)
$transaction(...) no wrapper yet. Use driver sessions on a replica set (see 3d)
$connect / $disconnect connects lazily on first use. Call db.close() to disconnect

v6 MongoDB had no Prisma Migrate, only db push (ephemeral, no history). Prisma ORM 8 gives MongoDB first-class, contract-driven migrations: reviewable diffs you commit, reproducible across environments, and an auditable schema history.

Your database already has collections and data, so bootstrap it with db update. The command diffs the live database against your contract, applies the delta (the strict validators, plus any indexes the contract declares that v6 never created), and signs the database so Prisma ORM 8 recognizes it from then on. The --advance-ref db flag records the resulting contract state as the db ref, the starting point later migration plans diff from.

npx prisma contract emit

npx prisma db update --db "$DATABASE_URL" --dry-run   # preview the delta first

npx prisma db update --db "$DATABASE_URL" --advance-ref db

Don't reach for db init here. It bootstraps only additively, and on a populated database it refuses the validator changes this migration needs, because adding a validator to a collection with existing documents is classed destructive. db update applies them after a confirmation prompt.

Use db update for local experiments (the db push analogue, no history kept) and the migration workflow for shared branches and production:

npx prisma contract emit                             # planning diffs the emitted contract

npx prisma migration plan --name add_posts_indexes   # plans from the db ref

npx prisma db migrate --db "$DATABASE_URL" --advance-ref db

npx prisma db verify --db "$DATABASE_URL"   # check the DB matches the contract

Good to know:

  • You never hand-write migration steps: declare indexes and validators in the contract (step 2) and migration plan derives the changes.
  • If a migrate run is interrupted, rerun it to resume. After fixing anything by hand, run db sign so the signature matches the database again.
  • Prisma ORM 8 adds strict $jsonSchema validators by default. Make sure existing documents pass them before running in production: once the validators are live, writes to documents that don't match the contract fail with Document failed validation.

Work through this list before you switch production traffic over:

  • Same database. Your Prisma ORM 8 config points at the same database and connection string as v6.
  • Server version. Your MongoDB server is on 8.0 or newer.
  • Dry run first. Rehearse the whole flow on a throwaway copy of your database (contract emit, db update --dry-run, db update, db verify) and make sure db verify passes before you touch production.
  • Index parity. The indexes on each collection (db.collection.getIndexes()) match your contract.
  • Validators. Your existing documents pass the strict validators Prisma ORM 8 adds to each collection (see Good to know in step 4).
  • Addressing. Every call site uses storage names like db.orm.users, not model names.
  • Transactions. Every v6 $transaction now has a driver-session equivalent.
  • Raw calls. Every $runCommandRaw, findRaw, and aggregateRaw has a pipeline-builder or mongodb-driver replacement.
  • Read-only trial run. Run Prisma ORM 8 read-only alongside your v6 app for a while and compare the results. This surfaces any differences while v6 is still handling all the writes.
  • Cutover and rollback. Switch writes over only once the trial run looks good, and keep the v6 branch deployable. Rolling back is just a code rollback since your data never moved.

Don't delete the v6 client, schema, or dependencies until this checklist passes.

Once you've cut over, this is the day-to-day workflow:

  • Schema changes. Edit the contract, then run contract emit, migration plan, and migrate (same loop from step 4).
  • Troubleshooting. db verify checks the database against your contract, and db sign records any fixes you make by hand.
  • Performance. Your aggregations now compile to native MongoDB pipelines, so it's worth benchmarking them against your old v6 raw calls.

See the Prisma ORM docs for pipeline builder patterns, relation loading strategies, and advanced contract features. This guide gets you across the bridge. From here on, the Prisma ORM docs are the source of truth.

Suggest an edit

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

Export
Documentation menu