Skip to main content

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.prisma file instead of inline model definitions
  • Full TypeScript types: Prisma generates exact types, no any or 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.