How to Migrate from Mongoose to Prisma
How do I migrate from Mongoose to Prisma?
TL;DR
- Bottom line: Install Prisma alongside Mongoose, introspect your MongoDB database with
prisma db pull, convert Mongoose schemas to Prisma models (mapping_idwith@map("_id") @db.ObjectId), then incrementally replace Mongoose queries with type-safe Prisma Client calls. - Key tool/command:
npx prisma db pull(introspects existing MongoDB into a Prisma schema) - Watch out for: Prisma Migrate (
prisma migrate) does not support MongoDB — always useprisma db pushto sync schema changes. Also: Prisma v7 does not yet support MongoDB — stay on v6.19. - Works with: Prisma 6.x (use v6.19 for MongoDB), MongoDB 4.2+, Node.js 18+, TypeScript 5.x.
Constraints
- Prisma ORM v7 does not support MongoDB — use Prisma v6.19 (latest v6) for all MongoDB projects until v7 MongoDB support ships. [src7]
prisma migrate devandprisma migrate deployare not supported for MongoDB — always useprisma db pushto sync schema changes. [src3]- MongoDB must run as a replica set for Prisma transactions — standalone
mongodwill fail on anyprisma.$transaction()call. [src3] - Prisma introspection (
prisma db pull) should be run once for initial schema — repeated runs overwrite custom relations, composite types, and@ignoreattributes. [src1] - Composite types in Prisma MongoDB views cause Engine Panic in Prisma 6.13+ — remove composite type fields from view definitions. [src4]
- Never instantiate
PrismaClientper request — use a singleton; eachnew PrismaClient()opens a fresh connection pool. [src2]
Quick Reference
| Mongoose Pattern | Prisma Equivalent | Example |
|---|---|---|
new Schema({ name: String }) | model User { name String } | Define in schema.prisma instead of JS |
mongoose.model('User', schema) | prisma.user | Auto-generated from schema, no manual model registration |
User.create({ name }) | prisma.user.create({ data: { name } }) | Wrap fields in data: object |
User.findById(id) | prisma.user.findUnique({ where: { id } }) | Use where: clause with unique field |
User.find({ age: { $gt: 18 } }) | prisma.user.findMany({ where: { age: { gt: 18 } } }) | Replace $gt with gt (no dollar prefix) |
User.findOneAndUpdate(filter, update) | prisma.user.update({ where, data }) | Separate where and data arguments |
User.deleteOne({ _id: id }) | prisma.user.delete({ where: { id } }) | Use id not _id (mapped via @map) |
User.find().populate('posts') | prisma.user.findMany({ include: { posts: true } }) | Type-safe eager loading via include |
User.find().select('name email') | prisma.user.findMany({ select: { name: true, email: true } }) | Boolean field selection |
User.find().sort({ name: 1 }) | prisma.user.findMany({ orderBy: { name: 'asc' } }) | Replace 1/-1 with 'asc'/'desc' |
User.find().skip(10).limit(5) | prisma.user.findMany({ skip: 10, take: 5 }) | limit becomes take |
User.countDocuments(filter) | prisma.user.count({ where: filter }) | Same filter syntax as findMany |
Schema.pre('save', fn) | Prisma Client extensions ($extends) | Use $extends — $use() middleware removed in Prisma 7 |
subdoc: { type: AddressSchema } | type Address { ... } + address Address | Use type keyword for embedded docs |
Decision Tree
START
├── Is your MongoDB running as a replica set?
│ ├── NO → Enable replica set first (required for Prisma transactions)
│ └── YES ↓
├── Are you on Prisma v7?
│ ├── YES → Prisma v7 doesn't support MongoDB yet — downgrade to v6.19
│ └── NO (v6.x) ↓
├── Do you need Prisma Migrate (SQL-style migrations)?
│ ├── YES → Prisma Migrate doesn't support MongoDB — use prisma db push instead
│ └── NO ↓
├── Does your Mongoose schema use middleware (pre/post hooks)?
│ ├── YES → Convert to Prisma Client extensions ($extends) — not $use() (removed in v7)
│ └── NO ↓
├── Does your schema use embedded subdocuments?
│ ├── YES → Convert to Prisma composite types (use type keyword in schema)
│ └── NO ↓
├── Does your schema use discriminators (polymorphic models)?
│ ├── YES → Prisma doesn't support discriminators — flatten into separate models
│ └── NO ↓
└── DEFAULT → Introspect with prisma db pull, add relations manually, replace queries incrementally
Decision Logic
Agent-facing if/then rules for recommending a Mongoose-to-Prisma path. Apply top-down; the first matching rule wins.
If your project uses MongoDB
--> Pin Prisma to v6.19 (the latest v6). Prisma v7 (GA January 2026) shipped without MongoDB; MongoDB support is coming via Prisma Next, not v7. Do not run npm install prisma@latest. [src9, src7]
If you are already on Prisma v7 and need MongoDB
--> Downgrade to prisma@6 + @prisma/client@6. v7's Rust-free engine and ESM-only client do not yet carry the MongoDB connector. Track the connector/mongodb label and watch for an RC that explicitly names MongoDB. [src9, src10]
If your Mongoose schema uses embedded subdocuments
--> Convert each subdocument to a Prisma composite type block (MongoDB connector only) — not a separate model. Note findUnique() cannot filter composite fields and aggregation does not support them; use findFirst() instead. [src4]
If your Mongoose schema uses discriminators (polymorphic models)
--> Prisma has no discriminator equivalent. Flatten the variants into separate models, or use a single model with a type enum field plus nullable variant fields. Decide before introspection. [src1, src5]
If your code relies on Mongoose middleware (pre/post hooks)
--> Reimplement with Prisma Client extensions ($extends) at the query level, or move the logic into your service layer. Do not reach for $use() — it is removed in Prisma 7 and deprecated in v6. [src2, src7]
If you need SQL-style migration history (versioned, reviewable)
--> You cannot get it on MongoDB: prisma migrate dev/deploy are unsupported for the MongoDB connector. Use prisma db push and version your schema.prisma in git as the source of truth. [src3]
If your MongoDB runs as a standalone mongod (no replica set)
--> Enable a replica set before relying on prisma.$transaction() — standalone instances fail every transactional call. Atlas clusters are replica sets by default; locally use mongod --replSet rs0. [src3]
Step-by-Step Guide
1. Install Prisma alongside Mongoose
Add Prisma to your existing project without removing Mongoose. Both ORMs can coexist and query the same database during migration. [src1]
npm install @prisma/client@6
npm install -D prisma@6
npx prisma init --datasource-provider mongodb
Verify: ls prisma/schema.prisma exists and contains provider = "mongodb".
2. Configure the database connection
Set your MongoDB connection string in the .env file. The format is identical to what Mongoose uses. [src3, src6]
# .env
DATABASE_URL="mongodb+srv://username:[email protected]/mydb?retryWrites=true&w=majority"
If you get SCRAM failure: Authentication failed, append ?authSource=admin to the connection string.
Verify: npx prisma db pull connects without errors.
3. Introspect the existing MongoDB database
Prisma samples up to 1,000 documents per collection to infer the schema. Ensure your database has representative data before running introspection. Fields with inconsistent types across documents will be mapped to Json with a warning. [src1, src3]
npx prisma db pull
This generates models in prisma/schema.prisma. Review the output — it will have no relations defined. You must add those manually.
Verify: Open prisma/schema.prisma — each collection should appear as a model block.
4. Add relations and fix the schema
Introspection won't detect relations or embedded documents automatically. Add @relation attributes for references between collections, and convert embedded subdocuments to composite type blocks. Map collection names with @@map() since Mongoose auto-pluralizes. [src1, src4, src5]
// prisma/schema.prisma
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
name String?
posts Post[]
address Address? // embedded document (composite type)
v Int? @map("__v") @ignore // Mongoose version key
@@map("users") // Mongoose auto-pluralizes collection names
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String?
authorId String @db.ObjectId
author User @relation(fields: [authorId], references: [id])
@@map("posts")
}
// Composite type for embedded documents
type Address {
street String
city String
zip String
}
Verify: npx prisma validate returns no errors.
5. Generate the Prisma Client and push schema
Generate the type-safe client and sync indexes/constraints to MongoDB. [src1]
npx prisma generate
npx prisma db push
Verify: npx prisma studio opens a GUI showing your collections and data.
6. Replace Mongoose queries with Prisma Client
Migrate one route/service at a time. Import PrismaClient alongside your Mongoose models and switch queries incrementally. [src1, src2]
// BEFORE: Mongoose
const mongoose = require('mongoose');
const User = mongoose.model('User');
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id).populate('posts');
res.json(user);
});
// AFTER: Prisma
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
app.get('/users/:id', async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: req.params.id },
include: { posts: true },
});
res.json(user);
});
Verify: API responses match between Mongoose and Prisma versions for the same endpoints.
7. Remove Mongoose and clean up
Once all queries are migrated, uninstall Mongoose and remove schema files. [src1]
npm uninstall mongoose
# Delete Mongoose model files (e.g., models/User.js, models/Post.js)
# Remove mongoose.connect() from your entry point
grep -rn 'mongoose' --include='*.js' --include='*.ts' | grep -v node_modules
Verify: grep returns zero results. App runs without Mongoose loaded.
Code Examples
JavaScript: Converting a Mongoose CRUD service to Prisma
Full script: javascript-converting-a-mongoose-crud-service-to-p.js (61 lines)
// Input: Express route handlers using Mongoose for User CRUD
// Output: Same handlers using Prisma Client with full error handling
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// CREATE
async function createUser(req, res) {
try {
const user = await prisma.user.create({
data: {
email: req.body.email,
name: req.body.name,
address: { set: { street: req.body.street, city: req.body.city, zip: req.body.zip } },
},
});
res.status(201).json(user);
} catch (err) {
if (err.code === 'P2002') res.status(409).json({ error: 'Email already exists' });
else res.status(500).json({ error: err.message });
}
}
TypeScript: Converting Mongoose middleware to Prisma Client extensions
Full script: typescript-converting-mongoose-middleware-to-prism.ts (49 lines)
// Input: Mongoose pre-save hooks and virtuals
// Output: Prisma Client extensions that replicate the same behavior
import { PrismaClient } from '@prisma/client';
// Mongoose middleware equivalent via Prisma Client extensions
const prisma = new PrismaClient().$extends({
query: {
user: {
async create({ args, query }) {
if (args.data.email) {
args.data.email = (args.data.email as string).toLowerCase().trim();
}
args.data.createdAt = args.data.createdAt ?? new Date();
args.data.updatedAt = new Date();
return query(args);
},
},
},
});
TypeScript: Converting Mongoose schema with embedded documents to Prisma
Full script: typescript-converting-mongoose-schema-with-embedde.ts (68 lines)
// Input: Mongoose schema with nested subdocuments and arrays
// Output: Equivalent Prisma schema + TypeScript query code
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Create order with embedded documents
async function createOrder(customerId: string) {
return prisma.order.create({
data: {
customerId,
items: [
{ product: 'Keyboard', quantity: 1, price: 79.99 },
{ product: 'Mouse', quantity: 2, price: 29.99 },
],
shippingAddress: { set: { street: '123 Main St', city: 'Berlin', country: 'DE' } },
status: 'pending',
},
include: { customer: true },
});
}
Anti-Patterns
Wrong: Using prisma migrate with MongoDB
# ❌ BAD — prisma migrate does not support MongoDB
# This will fail with: "Error: Prisma Migrate does not support MongoDB"
npx prisma migrate dev --name add_users
Correct: Use prisma db push for MongoDB schema changes
# ✅ GOOD — db push syncs your schema to MongoDB without migration files
npx prisma db push
Wrong: Using _id directly in Prisma queries
// ❌ BAD — Prisma maps _id to id via @map("_id")
const user = await prisma.user.findUnique({
where: { _id: '507f1f77bcf86cd799439011' }, // Error: Unknown field _id
});
Correct: Use the mapped field name id
// ✅ GOOD — Use the Prisma field name, not the MongoDB field name
const user = await prisma.user.findUnique({
where: { id: '507f1f77bcf86cd799439011' },
});
Wrong: Instantiating PrismaClient per request
// ❌ BAD — Creates a new connection pool on every request
app.get('/users', async (req, res) => {
const prisma = new PrismaClient(); // New instance per request!
const users = await prisma.user.findMany();
res.json(users);
});
Correct: Use a singleton PrismaClient instance
// ✅ GOOD — Single instance reused across all requests
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
process.on('beforeExit', async () => {
await prisma.$disconnect();
});
app.get('/users', async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
});
Wrong: Manually converting ObjectId strings
// ❌ BAD — Manually creating ObjectId like in Mongoose
const { ObjectId } = require('mongodb');
const user = await prisma.user.findUnique({
where: { id: new ObjectId('507f1f77bcf86cd799439011') },
});
Correct: Pass plain strings — Prisma handles ObjectId conversion
// ✅ GOOD — Prisma auto-converts strings to ObjectId when @db.ObjectId is declared
const user = await prisma.user.findUnique({
where: { id: '507f1f77bcf86cd799439011' },
});
Wrong: Using Mongoose-style $operators in Prisma filters
// ❌ BAD — Mongoose/MongoDB query syntax doesn't work in Prisma
const users = await prisma.user.findMany({
where: { age: { $gte: 18, $lte: 65 } },
});
Correct: Use Prisma's filter syntax without dollar prefix
// ✅ GOOD — Prisma uses its own filter operators
const users = await prisma.user.findMany({
where: { age: { gte: 18, lte: 65 } },
});
Wrong: Using prisma.$use() middleware (removed in Prisma 7)
// ❌ BAD — $use() middleware is deprecated in v6 and removed in v7
prisma.$use(async (params, next) => {
params.args.data.updatedAt = new Date();
return next(params);
});
Correct: Use Prisma Client extensions ($extends)
// ✅ GOOD — $extends is the stable API for query interception
const prisma = new PrismaClient().$extends({
query: {
$allModels: {
async $allOperations({ args, query }) {
if (args.data) args.data.updatedAt = new Date();
return query(args);
},
},
},
});
Common Pitfalls
- Introspection returns no relations: MongoDB has no foreign key constraints, so
prisma db pullcannot detect references between collections. Fix: Manually add@relation(fields: [...], references: [...])attributes and reference fields with@db.ObjectId. [src1] - Mongoose
__vversion key breaks Prisma schema: Mongoose adds a__vfield to every document. After introspection, Prisma includes it but doesn't need it. Fix: Add@ignoreto thevfield:v Int? @map("__v") @ignore. [src5] - Collection name mismatch: Mongoose auto-pluralizes and lowercases model names (e.g.,
Userbecomesusers). Prisma uses the model name as-is. Fix: Add@@map("users")to your Prisma model to match the existing collection name. [src5] - Replica set required for transactions: Prisma transactions require a MongoDB replica set. Standalone instances will fail. Fix: Use
mongod --replSet rs0locally or ensure your Atlas cluster is a replica set (default for Atlas). [src3] - Mixed data types in fields: MongoDB allows different types in the same field across documents. Prisma enforces a single type per field. Fix: Clean up inconsistent data before introspection, or review type conflict warnings — Prisma maps mixed-type fields to
Json. [src3] - Composite type query limitations:
findUnique()cannot filter on composite type fields, and aggregation operations do not support composite types. Fix: UsefindFirst()with composite type filters instead. [src4] - Composite types in views cause Engine Panic: Since Prisma 6.13, using composite types in MongoDB view definitions causes an Engine Panic. Fix: Remove composite type fields from
viewblocks or refactor views to exclude embedded document fields. [src4] - Mongoose middleware/hooks have no direct equivalent: Prisma does not have
pre('save')orpost('find')hooks. The$use()middleware API is deprecated in v6 and removed in v7. Fix: Use Prisma Client extensions ($extends) for query-level middleware, or move logic to your application service layer. [src2, src7]
Diagnostic Commands
# Check Prisma version and MongoDB connection
npx prisma --version
npx prisma db pull --print
# Validate your Prisma schema for errors
npx prisma validate
# Open Prisma Studio to browse data visually
npx prisma studio
# Generate the Prisma Client after schema changes
npx prisma generate
# Push schema changes to MongoDB (not migrate!)
npx prisma db push
# Check for remaining Mongoose references in codebase
grep -rn 'mongoose\|Schema\|\.populate(' --include='*.js' --include='*.ts' | grep -v node_modules | wc -l
# Check MongoDB replica set status (mongosh)
# rs.status()
# Verify Prisma is on v6 (not v7) for MongoDB projects
npx prisma --version | head -1
# Expected: prisma : 6.19.x
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Prisma 7.x (GA 2026-01) | Current (no MongoDB) | Rust-free TypeScript engine (90% smaller bundle, ~3x faster queries), ESM-only, generated client emitted to project source instead of node_modules, $use() removed, prisma-client replaces prisma-client-js generator | Do NOT use for MongoDB — MongoDB ships via Prisma Next, not v7; stay on v6.19 |
| Prisma 6.x (2025) | LTS for MongoDB | ESM-first client, new generator syntax | Use prisma@6 + @prisma/client@6 — v6.19 is latest MongoDB-compatible release |
| Prisma 5.x (2023) | Maintenance | jsonProtocol default, removed rejectOnNotFound | Update error handling for not-found queries |
| Prisma 4.x (2022) | EOL | extendedIndexes, filterJson GA | No MongoDB-specific breaking changes |
| Prisma 3.12+ (2022) | EOL | Composite types GA | First version with stable embedded document support |
| Mongoose 8.x (2024) | Current | Dropped Node.js 14 support | Note new default behaviors when migrating from 8.x |
| Mongoose 7.x (2023) | LTS | Dropped callback support | Convert callbacks to async/await before migrating to Prisma |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| You need full TypeScript type safety for queries | You rely heavily on Mongoose plugins | Keep Mongoose or wrap plugins |
| Your project uses multiple databases | Your app is deeply invested in aggregation pipelines | Mongoose with native driver for aggregation |
| You want auto-generated types from schema | You need MongoDB-specific features (change streams, GridFS, $graphLookup) | Mongoose or native MongoDB driver |
| Your team is building a new service alongside legacy | Your MongoDB data has highly inconsistent field types | Clean data first, then consider Prisma |
| You want visual database browsing via Prisma Studio | You need Mongoose-style middleware for complex logic | Use Prisma $extends or application-layer middleware |
| You want a single ORM for both SQL and MongoDB | Your project needs Prisma v7's Rust-free engine on MongoDB today | Stay on v6.19 and wait for MongoDB support in Prisma Next |
Important Caveats
- Prisma ORM v7 (GA January 2026, Rust-free TypeScript engine) does not support MongoDB. Use Prisma v6.19 (the latest v6 release) for full MongoDB compatibility. Per the official roadmap, MongoDB support is coming through Prisma Next — not v7 — with no announced ship date; track the
connector/mongodblabel for an RC. prisma migrate devandprisma migrate deployare not supported for MongoDB — always useprisma db pushto sync schema changes.- Prisma introspection for MongoDB is designed to be run once for the initial schema. Running it repeatedly will overwrite custom changes (relations, composite types,
@ignoreattributes). - MongoDB transactions require a replica set. Local development with a standalone
mongodinstance will fail on any operation that usesprisma.$transaction(). - Mongoose's
.lean()returns plain objects; Prisma Client always returns plain objects by default (no hydration overhead). - Composite types (embedded documents) are only available with the MongoDB connector and cannot be used with relational databases.
- The
prisma.$use()middleware API is deprecated in Prisma 6 and completely removed in Prisma 7. Use$extendsfor all new query interception logic. - When MongoDB support lands (via Prisma Next), a second upgrade from v6 will be needed — expect ESM-only, the new Rust-free client generated into your project source (not
node_modules), and the newprisma-clientgenerator syntax. Plan for this.