How to Migrate from Mongoose to Prisma
·PkgPulse Team
TL;DR
Migrating from Mongoose to Prisma means switching from document-oriented to schema-first ORM. If you're staying on MongoDB, Prisma now supports it natively. If switching to PostgreSQL (a common reason to migrate), you'll need to convert your documents to relational schemas. In both cases, Prisma gives you fully-typed queries, generated migrations, and Prisma Studio for free. The hardest part isn't the syntax — it's the data model translation.
Key Takeaways
- Schema-first: Prisma uses
schema.prismafile instead of inline model definitions - Full TypeScript types: Prisma generates exact types, no
anyor manual typing - Prisma supports MongoDB: Can migrate Mongoose → Prisma without changing databases
- Relational shift: If moving to PostgreSQL, embedded documents → join tables
- Prisma Studio: Free GUI for your database (replacing Mongo Compass for development)
Step 1: Install Prisma
npm install prisma @prisma/client
npx prisma init
# For PostgreSQL (recommended for relational data):
# DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# For MongoDB (staying on Mongo):
# DATABASE_URL="mongodb+srv://user:password@cluster.mongodb.net/mydb"
Step 2: Convert Mongoose Models to Prisma Schema
// BEFORE — Mongoose model definition
import mongoose, { Schema, Document } from 'mongoose';
interface IUser extends Document {
name: string;
email: string;
role: 'user' | 'admin';
profile: {
bio?: string;
avatar?: string;
website?: string;
};
posts: mongoose.Types.ObjectId[];
createdAt: Date;
updatedAt: Date;
}
const UserSchema = new Schema<IUser>(
{
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
profile: {
bio: String,
avatar: String,
website: String,
},
posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }],
},
{ timestamps: true }
);
export const User = mongoose.model<IUser>('User', UserSchema);
// AFTER — Prisma schema (PostgreSQL)
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String
email String @unique
role Role @default(user)
bio String? // Flat — embedded doc fields become columns
avatar String?
website String?
posts Post[] // Relation — replaces ObjectId array
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum Role {
user
admin
}
model Post {
id String @id @default(cuid())
title String
content String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Step 3: Query Translation
CRUD Operations
// ─── CREATE ───────────────────────────────────────────────────────────
// Mongoose:
const user = await User.create({
name: 'Alice',
email: 'alice@example.com',
role: 'admin',
});
// Prisma:
const user = await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@example.com',
role: 'admin',
},
});
// ─── READ ─────────────────────────────────────────────────────────────
// Mongoose:
const user = await User.findById(id);
const user = await User.findOne({ email: 'alice@example.com' });
const users = await User.find({ role: 'admin' }).sort({ name: 1 }).limit(10);
// Prisma:
const user = await prisma.user.findUnique({ where: { id } });
const user = await prisma.user.findUnique({ where: { email: 'alice@example.com' } });
const users = await prisma.user.findMany({
where: { role: 'admin' },
orderBy: { name: 'asc' },
take: 10,
});
// ─── UPDATE ───────────────────────────────────────────────────────────
// Mongoose:
await User.findByIdAndUpdate(id, { $set: { name: 'Bob' } }, { new: true });
await User.updateMany({ role: 'user' }, { $set: { active: true } });
// Prisma:
await prisma.user.update({ where: { id }, data: { name: 'Bob' } });
await prisma.user.updateMany({ where: { role: 'user' }, data: { active: true } });
// ─── DELETE ───────────────────────────────────────────────────────────
// Mongoose:
await User.findByIdAndDelete(id);
await User.deleteMany({ role: 'admin' });
// Prisma:
await prisma.user.delete({ where: { id } });
await prisma.user.deleteMany({ where: { role: 'admin' } });
Relations and Populate
// Mongoose — populate (N+1 risk):
const user = await User.findById(id).populate('posts');
// Prisma — include (single JOIN query):
const user = await prisma.user.findUnique({
where: { id },
include: {
posts: true, // Join posts in single query
},
});
// Nested includes:
const user = await prisma.user.findUnique({
where: { id },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10,
include: {
tags: true,
},
},
},
});
// Select specific fields (reduces payload):
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
posts: {
select: { id: true, title: true },
},
},
});
Step 4: Mongoose Virtuals → Prisma Computed Fields
// Mongoose virtual:
UserSchema.virtual('fullName').get(function () {
return `${this.firstName} ${this.lastName}`;
});
// Prisma: no built-in virtuals — handle in application layer
const user = await prisma.user.findUnique({ where: { id } });
const fullName = `${user.firstName} ${user.lastName}`;
// Or create a helper function:
function getFullName(user: { firstName: string; lastName: string }) {
return `${user.firstName} ${user.lastName}`;
}
Step 5: Mongoose Middleware → Prisma Middleware
// Mongoose pre-save hook:
UserSchema.pre('save', async function () {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 12);
}
});
// Prisma middleware (older API):
prisma.$use(async (params, next) => {
if (params.model === 'User' && params.action === 'create') {
params.args.data.password = await bcrypt.hash(params.args.data.password, 12);
}
return next(params);
});
// Prisma extensions (newer API):
const prismaWithHooks = prisma.$extends({
query: {
user: {
async create({ args, query }) {
if (args.data.password) {
args.data.password = await bcrypt.hash(args.data.password, 12);
}
return query(args);
},
},
},
});
Step 6: Create Migrations
# Generate Prisma Client from schema
npx prisma generate
# Create and apply database migration
npx prisma migrate dev --name init
# Inspect your database with Prisma Studio
npx prisma studio
# Opens browser UI at localhost:5555
# For production:
npx prisma migrate deploy
Compare Mongoose and Prisma on PkgPulse.
See the live comparison
View mongoose vs. prisma on PkgPulse →