Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Mongoose

This guide shows you how to migrate an application from Mongoose to Prisma ORM. The migration is gradual and runs against the database you already have: you add Prisma ORM next to Mongoose, describe the collections Mongoose manages in a contract, replace queries route by route, and remove Mongoose when nothing calls it anymore. Your data never moves.

The examples use a small Express API with three Mongoose models (User, Category, Post) and four routes. Every command, code block, and response below was run end to end against a local MongoDB replica set.

  • A Mongoose project you want to migrate (this guide uses TypeScript and Express)
  • Node.js 24 or later
  • A MongoDB replica set. MongoDB Atlas is one already; locally, start mongod with --replSet rs0 and run rs.initiate() once
  • Basic familiarity with Mongoose

To delegate this guide to your coding agent, copy the prompt below and hand it over:

with
Migrate this Mongoose app to Prisma ORM against the same MongoDB database, following https://www.prisma.io/docs/guides/switch-to-prisma-orm/from-mongoose.md.

1. Run `npx prisma@latest orm init --target mongodb` in the project root, choose PSL, then run `npx prisma@latest init` so the Prisma agent skills are installed, and use them. Make sure `.env` has DATABASE_URL pointing at the database the Mongoose app already uses.

2. `contract infer` does not support MongoDB. Write `src/prisma/contract.prisma` by hand from the Mongoose schemas: one model per collection with `id ObjectId @id @map("_id")` and `@@map("<collection>")`, a `type` block per nested schema, `ObjectId` plus `@relation` for each `ref`, and `ObjectId[]` for arrays of refs. Then run `npx prisma contract emit`.

3. Replace the Mongoose queries route by route with `db.orm.<collection>` calls and `.include(...)` for `populate` on single references. For `populate` on arrays of references, use the pipeline builder with `.lookup(...)`. Run the app and verify every route with curl before and after.

4. When no code imports mongoose: run `npm uninstall mongoose && npm install mongodb`, remove the `__v` field from all documents, preview the validators with `npx prisma db update --dry-run`, apply the previewed collMod statements, then run `npx prisma db sign` and `npx prisma db verify`.

The steps for migrating from Mongoose to Prisma ORM are the same for any application, whether it is a REST API with Express, a GraphQL server, or a background worker:

  1. Add Prisma ORM to the project with orm init
  2. Describe the collections Mongoose manages in a contract and emit it
  3. Replace Mongoose queries with Prisma ORM queries, one route at a time
  4. Remove Mongoose and let Prisma ORM own the schema

Prisma ORM does not generate a client. There is no prisma generate step and no schema.prisma: contract emit writes the contract to contract.json and contract.d.ts, and your app reads those two files.

The app has three Mongoose models. User has a nested profile, Post references a User and an array of Category documents:

title="src/models/user.ts"
import { Schema, model } from "mongoose";

const userSchema = new Schema({

  email: { type: String, required: true, unique: true },

  name: { type: String, required: true },

  profile: { bio: String },

});

export const User = model("User", userSchema);
title="src/models/post.ts"
import { Schema, model } from "mongoose";

const postSchema = new Schema({

  title: { type: String, required: true },

  content: { type: String, required: true },

  published: { type: Boolean, default: false },

  author: { type: Schema.Types.ObjectId, ref: "User", required: true },

  categories: [{ type: Schema.Types.ObjectId, ref: "Category" }],

});

export const Post = model("Post", postSchema);

The Express server exposes GET /users, GET /users/:id, POST /users, and GET /posts, which populates author and categories:

title="src/server.ts"
import express from "express";

import mongoose from "mongoose";

import { User } from "./models/user";

import { Post } from "./models/post";

import "./models/category";

await mongoose.connect(process.env.DATABASE_URL!);

const app = express();

app.use(express.json());

app.get("/users", async (_req, res) => {

  const users = await User.find().sort({ name: 1 });

  res.json(users);

});

app.get("/users/:id", async (req, res) => {

  const user = await User.findById(req.params.id);

  if (!user) return res.status(404).json({ error: "Not found" });

  res.json(user);

});

app.post("/users", async (req, res) => {

  const user = await User.create({ email: req.body.email, name: req.body.name });

  res.status(201).json(user);

});

app.get("/posts", async (_req, res) => {

  const posts = await Post.find({ published: true }).populate("author").populate("categories");

  res.json(posts);

});

const port = Number(process.env.PORT ?? 3000);

app.listen(port, () => console.log(`Listening on http://localhost:${port}`));

The database is seeded with two users, two categories, and three posts. With the Mongoose server running, GET /users returns:

[{"profile":{"bio":"Writes about databases"},"_id":"6aa2cd2ed97f8d7357dc9bd1","email":"alice@prisma.io","name":"Alice","__v":0},{"_id":"6aa2cd2ed97f8d7357dc9bd2","email":"bob@prisma.io","name":"Bob","__v":0}]

Keep this response to compare against later, because the Prisma ORM version of the route returns the same documents.

Run orm init in the project root. It adds Prisma ORM to the existing app; it does not scaffold a new one:

bunx prisma@latest orm init --target mongodb
Bash
pnpm dlx prisma@latest orm init --target mongodb
Bash
yarn dlx prisma@latest orm init --target mongodb
Bash
npx prisma@latest orm init --target mongodb

Choose PSL when asked for the contract authoring style and keep the default schema path. The command installs the packages, writes the Prisma ORM files, and emits a starter contract:

no-copy
✔ npm add @prisma/orm-mongo dotenv
✔ npm add -D prisma@latest
✔ npm add -D @prisma/cli-engine@0.6.1
✔ Emit the contract
│  target:     mongodb
│  authoring:  psl
│  schema:     src/prisma/contract.prisma
written
├─ src/prisma/contract.prisma
├─ prisma.config.ts
├─ src/prisma/db.ts
├─ prisma-8.md
├─ .env.example
├─ tsconfig.json
├─ .gitignore
├─ .gitattributes
└─ package.json
installed
├─ @prisma/orm-mongo
├─ dotenv
├─ prisma@latest (dev)
└─ @prisma/cli-engine@0.6.1 (dev)
✔ Done. Open prisma-8.md to get started.

@prisma/orm-mongo is the library your app imports to query MongoDB. It uses the mongodb driver, the same one Mongoose already depends on, so you do not need a second database driver.

orm init also changes the module settings in tsconfig.json, and adds "type": "module" to package.json when the file has no "type" field. If your app is CommonJS, follow In a CommonJS project before you start the server again.

Install the Prisma agent skills for your coding agent with init, which also adds a postinstall hook that resyncs them on every install:

Choose PSL when asked for the contract authoring style and keep the default schema path. The command installs the packages, writes the Prisma ORM files, and emits a starter contract:

✔ npm add @prisma/orm-mongo dotenv

✔ npm add -D prisma@latest

✔ npm add -D @prisma/cli-engine@0.6.1

✔ Emit the contract

│  target:     mongodb

│  authoring:  psl

│  schema:     src/prisma/contract.prisma

written

├─ src/prisma/contract.prisma

├─ prisma.config.ts

├─ src/prisma/db.ts

├─ prisma-8.md

├─ .env.example

├─ tsconfig.json

├─ .gitignore

├─ .gitattributes

└─ package.json

installed

├─ @prisma/orm-mongo

├─ dotenv

├─ prisma@latest (dev)

└─ @prisma/cli-engine@0.6.1 (dev)

✔ Done. Open prisma-8.md to get started.

@prisma/orm-mongo is the library your app imports to query MongoDB. It uses the mongodb driver, the same one Mongoose already depends on, so you do not need a second database driver.

orm init also changes the module settings in tsconfig.json, and adds "type": "module" to package.json when the file has no "type" field. If your app is CommonJS, follow In a CommonJS project before you start the server again.

Install the Prisma agent skills for your coding agent with init, which also adds a postinstall hook that resyncs them on every install:

title="bun"
bunx --bun prisma@latest init
pnpm
pnpm dlx prisma@latest init
yarn
yarn dlx prisma@latest init
npm
npx prisma@latest init

If a later CLI command reports that the skills are out of date, run npx prisma skills sync.

orm init wrote a prisma.config.ts that loads .env and points at the contract:

title="prisma.config.ts"
import 'dotenv/config';

import { definePrismaConfig } from '@prisma/cli-engine';

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']!,

    },

  }),

});

It also wrote src/prisma/db.ts, the client the rest of the app imports. There is no adapter and no engine; the factory takes the emitted contract and the connection string:

title="src/prisma/db.ts"
import 'dotenv/config';

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

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

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

export const db = mongo<Contract>({

  contractJson,

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

});

Both read DATABASE_URL, so point it at the database your Mongoose app already uses. orm init writes an .env.example but never touches an existing .env:

title=".env"
DATABASE_URL="mongodb://localhost:27017/blog?replicaSet=rs0&directConnection=true"

On PostgreSQL, contract infer reads the live schema and writes a starter contract. MongoDB has no schema to read, so the command stops:

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
✘ [CONTRACT.INFER_UNSUPPORTED] contract infer is not supported for this family
  why: The configured family does not implement the PslContractInferCapable capability, so an inferred PSL contract cannot be produced from the live database schema.

Your Mongoose schemas are the source of truth instead. Translate them with these rules:

MongoosePrisma ORM contract
model("User", schema) stores in usersmodel User { ... @@map("users") }
_id (implicit)id ObjectId @id @map("_id")
{ type: String, required: true }name String
{ type: String }name String?
{ type: String, unique: true }email String @unique
nested schema profile: { bio: String }type Profile { bio String? } and profile Profile?
{ type: ObjectId, ref: "User" }authorId ObjectId @map("author") plus author User @relation(fields: [authorId], references: [id])
[{ type: ObjectId, ref: "Category" }]categoryIds ObjectId[] @map("categories")

Mongoose stores a model named User in the users collection, so every model needs @@map with the pluralized, lowercased collection name. Replace the starter contract with the models from the example app:

src/prisma/contract.prisma
// use prisma-8

type Profile {
  bio String?
}

model User {
  id      ObjectId @id @map("_id")
  email   String   @unique
  name    String
  profile Profile?
  posts   Post[]
  @@map("users")
}

model Category {
  id   ObjectId @id @map("_id")
  name String
  @@map("categories")
}

model Post {
  id          ObjectId   @id @map("_id")
  title       String
  content     String
  published   Bool
  author      User       @relation(fields: [authorId], references: [id])
  authorId    ObjectId   @map("author")
  categoryIds ObjectId[] @map("categories")
  @@map("posts")
}

Before you go on, check these differences from Mongoose:

  • Prisma ORM reads and writes MongoDB fields by their stored names. authorId @map("author") is exposed by the query API as author, the key Mongoose stores, and .include("author") fills it with the user document the way populate("author") does. db.orm.posts.where({ authorId }) is a type error.
  • Mongoose adds a __v version key to every document. It is not in the contract and Prisma ORM passes it through on reads. You remove it in step 4, before Prisma ORM adds validators.

You do not have to model every collection on day one. Start with the ones the routes you migrate first touch.

✘ [CONTRACT.INFER_UNSUPPORTED] contract infer is not supported for this family

  why: The configured family does not implement the PslContractInferCapable capability, so an inferred PSL contract cannot be produced from the live database schema.

Your Mongoose schemas are the source of truth instead. Translate them with these rules:

Mongoose Prisma ORM contract
model("User", schema) stores in users model User { ... @@map("users") }
_id (implicit) id ObjectId @id @map("_id")
{ type: String, required: true } name String
{ type: String } name String?
{ type: String, unique: true } email String @unique
nested schema profile: { bio: String } type Profile { bio String? } and profile Profile?
{ type: ObjectId, ref: "User" } authorId ObjectId @map("author") plus author User @relation(fields: [authorId], references: [id])
[{ type: ObjectId, ref: "Category" }] categoryIds ObjectId[] @map("categories")

Mongoose stores a model named User in the users collection, so every model needs @@map with the pluralized, lowercased collection name. Replace the starter contract with the models from the example app:

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

type Profile {

  bio String?

}

model User {

  id      ObjectId @id @map("_id")

  email   String   @unique

  name    String

  profile Profile?

  posts   Post[]

  @@map("users")

}

model Category {

  id   ObjectId @id @map("_id")

  name String

  @@map("categories")

}

model Post {

  id          ObjectId   @id @map("_id")

  title       String

  content     String

  published   Bool

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

  authorId    ObjectId   @map("author")

  categoryIds ObjectId[] @map("categories")

  @@map("posts")

}

Before you go on, check these differences from Mongoose:

  • Prisma ORM reads and writes MongoDB fields by their stored names. authorId @map("author") is exposed by the query API as author, the key Mongoose stores, and .include("author") fills it with the user document the way populate("author") does. db.orm.posts.where({ authorId }) is a type error.
  • Mongoose adds a __v version key to every document. It is not in the contract and Prisma ORM passes it through on reads. You remove it in step 4, before Prisma ORM adds validators.

You do not have to model every collection on day one. Start with the ones the routes you migrate first touch.

Turn the contract into the artifacts the runtime and the CLI read:

bunx prisma contract emit
Bash
pnpm prisma contract emit
Bash
yarn prisma contract emit
Bash
npx prisma contract emit
no-copy
✔ Resolving contract source...
✔ Emitting contract...
│  contract:  src/prisma/contract.json
│  types:     src/prisma/contract.d.ts

✔ Emitted contract.json and contract.d.ts

Run this again whenever you edit contract.prisma. Nothing else needs regenerating.

✔ Resolving contract source...

✔ Emitting contract...

│  contract:  src/prisma/contract.json

│  types:     src/prisma/contract.d.ts

✔ Emitted contract.json and contract.d.ts

Run this again whenever you edit contract.prisma. Nothing else needs regenerating.

Prisma ORM addresses collections by their storage name (db.orm.users, not db.orm.User) and chains a filter before every read or write except create:

// Find many, sorted

const users = await User.find().sort({ name: 1 });

// Find one by id

const user = await User.findById(id);

// Find one by field

const alice = await User.findOne({ email: "alice@prisma.io" });

// Create

const user = await User.create({ email: "alice@prisma.io", name: "Alice" });

// Update one

await User.findByIdAndUpdate(id, { name: "New name" });

// Delete one

await User.findByIdAndDelete(id);

// Populate a single reference

const posts = await Post.find({ published: true }).populate("author");
TypeScript
// Find many, sorted (1 ascending, -1 descending)
const users = await db.orm.users.orderBy({ name: 1 }).all();

// Find one by id
const user = await db.orm.users.where({ _id: id }).first();

// Find one by field
const alice = await db.orm.users.where({ email: "alice@prisma.io" }).first();

// Create; returns the inserted document with its _id
const user = await db.orm.users.create({ email: "alice@prisma.io", name: "Alice", profile: null });

// Update one; returns the updated document
await db.orm.users.where({ _id: id }).update({ name: "New name" });

// Delete one
await db.orm.users.where({ _id: id }).delete();

// Populate a single reference
const posts = await db.orm.posts.where({ published: true }).include("author").all();

.where(...) takes a plain object matched by equality. .update(...) and .delete() act on one document; use .updateAll(...) and .deleteAll() for many. Optional fields such as profile are required in the create input, so pass null when there is no value. For filters the object form does not cover, and for anything Mongoose did with aggregate, use the pipeline builder.

.where(...) takes a plain object matched by equality. .update(...) and .delete() act on one document; use .updateAll(...) and .deleteAll() for many. Optional fields such as profile are required in the create input, so pass null when there is no value. For filters the object form does not cover, and for anything Mongoose did with aggregate, use the pipeline builder.

.include(...) covers a single reference, but populate("categories") on an array of ids has no .include equivalent yet, so express it as a pipeline: .lookup(...) is a typed $lookup, and MongoDB matches an array on the local side against _id on the foreign side. Adding .unwind("author") after a lookup on a single reference turns the one-element array $lookup produces into the object Mongoose returns:

const runtime = await db.runtime();

const plan = db.query

  .from("posts")

  .match((f) => f.published.eq(true))

  .lookup((from) =>

    from("users")

      .on((post, user) => ({ local: post.author, foreign: user._id }))

      .as("author"),

  )

  .unwind("author")

  .lookup((from) =>

    from("categories")

      .on((post, category) => ({ local: post.categories, foreign: category._id }))

      .as("categories"),

  )

  .build();

const posts = await runtime.query(plan);

Run read plans with runtime.query(plan), because runtime.execute(plan) is for write plans and rejects an aggregate command.

Replace the Mongoose imports and the mongoose.connect call with the db client, then rewrite each handler. The finished server:

title="src/server.ts"
import express from "express";

import { db } from "./prisma/db";

const app = express();

app.use(express.json());

app.get("/users", async (_req, res) => {

  const users = await db.orm.users.orderBy({ name: 1 }).all();

  res.json(users);

});

app.get("/users/:id", async (req, res) => {

  const user = await db.orm.users.where({ _id: req.params.id }).first();

  if (!user) return res.status(404).json({ error: "Not found" });

  res.json(user);

});

app.post("/users", async (req, res) => {

  const user = await db.orm.users.create({

    email: req.body.email,

    name: req.body.name,

    profile: req.body.bio ? { bio: req.body.bio } : null,

  });

  res.status(201).json(user);

});

app.get("/posts", async (_req, res) => {

  const runtime = await db.runtime();

  const plan = db.query

    .from("posts")

    .match((f) => f.published.eq(true))

    .lookup((from) =>

      from("users")

        .on((post, user) => ({ local: post.author, foreign: user._id }))

        .as("author"),

    )

    .unwind("author")

    .lookup((from) =>

      from("categories")

        .on((post, category) => ({ local: post.categories, foreign: category._id }))

        .as("categories"),

    )

    .build();

  const posts = await runtime.query(plan);

  res.json(posts);

});

const port = Number(process.env.PORT ?? 3000);

app.listen(port, () => console.log(`Listening on http://localhost:${port}`));

There is no connect call: the client connects on the first query. Do not call db.close() in a handler; the connection pool is shared across requests and closes with the process.

Start the server and check the routes:

curl http://localhost:3000/users
[{"profile":{"bio":"Writes about databases"},"__v":0,"_id":"6aa2cd2ed97f8d7357dc9bd1","email":"alice@prisma.io","name":"Alice"},{"__v":0,"_id":"6aa2cd2ed97f8d7357dc9bd2","email":"bob@prisma.io","name":"Bob"}]

The same documents Mongoose returned, __v included, because both read the same collection.

curl http://localhost:3000/posts
[{"_id":"6aa2cd2ed97f8d7357dc9bd5","title":"Hello MongoDB","content":"First post","published":true,"author":{"_id":"6aa2cd2ed97f8d7357dc9bd1","email":"alice@prisma.io","name":"Alice","profile":{"bio":"Writes about databases"},"__v":0},"categories":[{"_id":"6aa2cd2ed97f8d7357dc9bd3","name":"Databases","__v":0}],"__v":0},{"_id":"6aa2cd2ed97f8d7357dc9bd7","title":"Bob's post","content":"Hi","published":true,"author":{"_id":"6aa2cd2ed97f8d7357dc9bd2","email":"bob@prisma.io","name":"Bob","__v":0},"categories":[],"__v":0}]
curl -X POST http://localhost:3000/users \

  -H "content-type: application/json" \

  -d '{"email":"carol@prisma.io","name":"Carol"}'
{"_id":"6aa2d3fcc0a1a4429529ba2c","email":"carol@prisma.io","name":"Carol","profile":null}

.create(...) returns the inserted document with its server-assigned _id, so the handler needs no second query.

You do not have to migrate every route in one go. Mongoose and Prisma ORM can read and write the same collections while you work through the app: Prisma ORM reads Mongoose documents as shown above, and Mongoose reads documents Prisma ORM created. After the POST /users call above, the untouched Mongoose GET /users route returns Carol too, without a __v key:

[{"profile":{"bio":"Writes about databases"},"_id":"6aa2cd2ed97f8d7357dc9bd1","email":"alice@prisma.io","name":"Alice","__v":0},{"_id":"6aa2cd2ed97f8d7357dc9bd2","email":"bob@prisma.io","name":"Bob","__v":0},{"profile":null,"_id":"6aa2d3fcc0a1a4429529ba2c","email":"carol@prisma.io","name":"Carol"}]

Hold off on db update, db sign, and migrations until step 4. They add strict validators to the collections, and Mongoose writes stop passing them.

When no module imports mongoose, remove it and keep the driver Prisma ORM needs. mongodb was a transitive dependency of Mongoose; make it a direct one:

title="bun"
bun remove mongoose

bun add mongodb
pnpm
pnpm remove mongoose
pnpm add mongodb
yarn
yarn remove mongoose
yarn add mongodb
npm
npm uninstall mongoose
npm install mongodb

Delete the models/ directory and any mongoose.connect call that is left.

Mongoose wrote __v on every document. The validators Prisma ORM adds in the next step reject fields the contract does not declare, so strip it first. In mongosh, against your database:

for (const c of ["users", "posts", "categories"]) {

  const r = db[c].updateMany({}, { $unset: { __v: "" } });

  print(c, r.modifiedCount);

}
users 2

posts 3

categories 2

If you cannot do this yet, add v Int? @map("__v") to every model instead and drop it later. The field must be optional, because documents Prisma ORM creates have no __v key.

Prisma ORM tracks a database by a signature and, on MongoDB, by a $jsonSchema validator per collection. Preview what it wants to apply:

bunx prisma db update --dry-run
Bash
pnpm prisma db update --dry-run
Bash
yarn prisma db update --dry-run
Bash
npx prisma db update --dry-run
no-copy
✔ Planned 3 operation(s) across 1 contract space

App space
├─ ⚠ Add validator on categories
├─ ⚠ Add validator on posts
└─ ⚠ Add validator on users

⚠ This migration contains destructive operations that may cause data loss.

ℹ Operation preview

db.runCommand({ collMod: "categories", validator: {"$jsonSchema":{"additionalProperties":false,"bsonType":"object","properties":{"_id":{"bsonType":"objectId"},"name":{"bsonType":"string"}},"required":["_id","name"]}}, validationLevel: "strict", validationAction: "error" })
db.runCommand({ collMod: "posts", validator: {"$jsonSchema":{"additionalProperties":false,"bsonType":"object","properties":{"_id":{"bsonType":"objectId"},"author":{"bsonType":"objectId"},"categories":{"bsonType":"array","items":{"bsonType":"objectId"}},"content":{"bsonType":"string"},"published":{"bsonType":"bool"},"title":{"bsonType":"string"}},"required":["_id","author","categories","content","published","title"]}}, validationLevel: "strict", validationAction: "error" })
db.runCommand({ collMod: "users", validator: {"$jsonSchema":{"additionalProperties":false,"bsonType":"object","properties":{"_id":{"bsonType":"objectId"},"email":{"bsonType":"string"},"name":{"bsonType":"string"},"profile":{"oneOf":[{"bsonType":"null"},{"additionalProperties":false,"bsonType":"object","properties":{"bio":{"bsonType":["null","string"]}}}]}},"required":["_id","email","name"]}}, validationLevel: "strict", validationAction: "error" })

ℹ This is a dry run. No changes were applied.

Adding a validator to a populated collection counts as destructive, so db update asks you to type the database name before applying it. On MongoDB it currently cannot resolve that name and stops with CLI.CONSENT_TOKEN_UNRESOLVED. Apply the three previewed collMod commands yourself: paste them into mongosh against your database. Each returns { ok: 1 }.

Then record that the database matches the contract, and verify it:

✔ Planned 3 operation(s) across 1 contract space

App space

├─ ⚠ Add validator on categories

├─ ⚠ Add validator on posts

└─ ⚠ Add validator on users

⚠ This migration contains destructive operations that may cause data loss.

ℹ Operation preview

db.runCommand({ collMod: "categories", validator: {"$jsonSchema":{"additionalProperties":false,"bsonType":"object","properties":{"_id":{"bsonType":"objectId"},"name":{"bsonType":"string"}},"required":["_id","name"]}}, validationLevel: "strict", validationAction: "error" })

db.runCommand({ collMod: "posts", validator: {"$jsonSchema":{"additionalProperties":false,"bsonType":"object","properties":{"_id":{"bsonType":"objectId"},"author":{"bsonType":"objectId"},"categories":{"bsonType":"array","items":{"bsonType":"objectId"}},"content":{"bsonType":"string"},"published":{"bsonType":"bool"},"title":{"bsonType":"string"}},"required":["_id","author","categories","content","published","title"]}}, validationLevel: "strict", validationAction: "error" })

db.runCommand({ collMod: "users", validator: {"$jsonSchema":{"additionalProperties":false,"bsonType":"object","properties":{"_id":{"bsonType":"objectId"},"email":{"bsonType":"string"},"name":{"bsonType":"string"},"profile":{"oneOf":[{"bsonType":"null"},{"additionalProperties":false,"bsonType":"object","properties":{"bio":{"bsonType":["null","string"]}}}]}},"required":["_id","email","name"]}}, validationLevel: "strict", validationAction: "error" })

ℹ This is a dry run. No changes were applied.

Adding a validator to a populated collection counts as destructive, so db update asks you to type the database name before applying it. On MongoDB it currently cannot resolve that name and stops with CLI.CONSENT_TOKEN_UNRESOLVED. Apply the three previewed collMod commands yourself: paste them into mongosh against your database. Each returns { ok: 1 }.

Then record that the database matches the contract, and verify it:

bunx prisma db sign

bunx prisma db verify
Bash
pnpm prisma db sign
pnpm prisma db verify
Bash
yarn prisma db sign
yarn prisma db verify
Bash
npx prisma db sign
npx prisma db verify
no-copy
✔ Database signed

from:  none
to:    7a7760fc1ac91a781b0f6509b8c495fa3f6ae2be1ba2815bceccbf75286b93c6
no-copy
✔ Database marker and schema match contract

From here on, schema changes go through the contract: edit contract.prisma, run contract emit, then db update for a direct development update or migration plan followed by db migrate for a checked-in migration.

The validators are strict: a write that includes a field the contract does not declare now fails, which is what a leftover Mongoose model produces:

no-copy
Document failed validation
{ operatorName: 'additionalProperties', specifiedAs: { additionalProperties: false }, additionalProperties: [ '__v' ] }
✔ Database signed

from:  none

to:    7a7760fc1ac91a781b0f6509b8c495fa3f6ae2be1ba2815bceccbf75286b93c6
✔ Database marker and schema match contract

From here on, schema changes go through the contract: edit contract.prisma, run contract emit, then db update for a direct development update or migration plan followed by db migrate for a checked-in migration.

The validators are strict: a write that includes a field the contract does not declare now fails, which is what a leftover Mongoose model produces:

Document failed validation

{ operatorName: 'additionalProperties', specifiedAs: { additionalProperties: false }, additionalProperties: [ '__v' ] }

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, translate the Mongoose schemas in src/models into src/prisma/contract.prisma and emit it."
  • "Replace the Mongoose queries in src/controllers/posts.ts with db.orm.posts calls; use a .lookup pipeline for populate on arrays."
  • "Add GET /users/:id/posts that returns a user's published posts with .where and .include."
Suggest an edit

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

Export
Documentation menu