Migrate from Mongoose

This guide describes how to migrate from Mongoose to Prisma. It uses an extended version of the Mongoose Express example as a sample project to demonstrate the migration steps. You can find the example used for this guide on GitHub.

You can learn how Prisma compares to Mongoose on the Prisma vs Mongoose page.

Overview of the migration process

Note that the steps for migrating from Mongoose to Prisma are always the same, no matter what kind of application or API layer you're building:

  1. Install the Prisma CLI
  2. Introspect your database
  3. Install Prisma Client
  4. Gradually replace your Mongoose queries with Prisma Client

These steps apply whether you're building a REST API (e.g. with Express, koa or NestJS), a GraphQL API (e.g. with Apollo Server, TypeGraphQL or Nexus) or any other kind of application that uses Mongoose for database access.

Prisma lends itself really well for incremental adoption. This means, you don't have migrate your entire project from Mongoose to Prisma at once, but rather you can step-by-step move your database queries from Mongoose to Prisma.

Overview of the sample project

For this guide, we'll use a REST API built with Express as a sample project to migrate to Prisma. It has three documents and one sub-document (embedded document):

post.js
user.js
category.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const PostSchema = new Schema({
title: String,
content: String,
published: {
type: Boolean,
default: false,
},
author: {
type: Schema.Types.ObjectId,
ref: 'author',
required: true,
},
categories: [
{
type: Schema.Types.ObjectId,
ref: 'Category',
},
],
})
module.exports = mongoose.model('Post', PostSchema)

The models/documents have the following types of relationships:

  • 1-n: User ↔ Post
  • m-n: Post ↔ Category
  • Sub-document/ Embedded document: User ↔ Profile

In the example used in this guide, the route handlers are located in the src/controllers directory. The models are located in the src/models directory. From there, the models are pulled into a central src/routes.js file, which is used to define the required routes in src/index.js:

└── blog-mongoose
├── package.json
└──src
   ├── controllers
   │   ├── post.js
   │   └── user.js
   ├── models
   │   ├── category.js
   │   ├── post.js
   │   └── user.js
   ├── index.js
   ├── routes.js
   └── seed.js

The example repository contains a seed script inside the package.json file.

Run npm run seed to populate your database with the sample data in the ./src/seed.js file.

Step 1. Install the Prisma CLI

The first step to adopt Prisma is to install the Prisma CLI in your project:

$npm install prisma --save-dev

Step 2. Introspect your database

Introspection is a process of inspecting the structure of a database, used in Prisma to generate a data model in your Prisma schema.

2.1. Set up Prisma

Before you can introspect your database, you need to set up your Prisma schema and connect Prisma to your database. Run the following command in your terminal to create a basic Prisma schema file:

$npx prisma init --datasource-provider mongodb

This command creates:

  • A new directory called prisma that contains a schema.prisma file; your Prisma schema file specifies your database connection and models
  • .env: A dotenv file at the root of your project (if it doesn't already exist), used to configure your database connection URL as an environment variable

The Prisma schema file currently looks as follows:

prisma/schema.prisma
1// This is your Prisma schema file,
2// learn more about it in the docs: https://pris.ly/d/prisma-schema
3
4datasource db {
5 provider = "mongodb"
6 url = env("DATABASE_URL")
7}
8
9generator client {
10 provider = "prisma-client-js"
11}

For an optimal development experience when working with Prisma, refer to editor setup to learn about syntax highlighting, formatting, auto-completion, and many more cool features.

2.2. Connect your database

Configure your database connection URL in the .env file.

The format of the connection URL that Mongoose uses is similar to the one Prisma uses.

.env
1DATABASE_URL="mongodb://alice:myPassword43@localhost:27017/blog-mongoose"

Refer to the MongoDB connection URL specification for further details.

2.3. Run Prisma's introspection

With your connection URL in place, you can introspect your database to generate your Prisma models:

Note: MongoDB is a schemaless database. To incrementally adopt Prisma in your project, ensure your database is populated with sample data. Prisma introspects a MongoDB schema by sampling data stored and inferring the schema from the data in the database.

$npx prisma db pull

This creates the following Prisma models:

prisma/schema.prisma
1type UsersProfile {
2 bio String
3}
4
5model categories {
6 id String @id @default(auto()) @map("_id") @db.ObjectId
7 v Int @map("__v")
8 name String
9}
10
11model posts {
12 id String @id @default(auto()) @map("_id") @db.ObjectId
13 v Int @map("__v")
14 author String @db.ObjectId
15 categories String[] @db.ObjectId
16 content String
17 published Boolean
18 title String
19}
20
21model users {
22 id String @id @default(auto()) @map("_id") @db.ObjectId
23 v Int @map("__v")
24 email String @unique(map: "email_1")
25 name String
26 profile UsersProfile?
27}

The generated Prisma models represent the MongoDB collections and are the foundation of your programmatic Prisma Client API which allows you to send queries to your database.

2.4. Update the relations

MongoDB doesn't support relations between different collections. However, you can create references between documents using the ObjectId field type or from one document to many using an array of ObjectIds in the collection. The reference will store id(s) of the related document(s). You can use the populate() method that Mongoose provides to populate the reference with the data of the related document.

Update the 1-n relationship between Post <-> User as follows:

  • Rename the existing author reference in the posts model to authorId and add the @map("author") attribute
  • Add the author relation field in the posts model and it's @relation attribute specifying the fields and references
  • Add the posts relation in the users model
diff
schema.prisma
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
}
model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean
v Int @map("__v")
author String @db.ObjectId
author users @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId
categories String[] @db.ObjectId
}
model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
posts posts[]
}

Update the m-n between Post <-> Category references as follows:

  • Rename the categories field to categoryIds and map it using @map("categories") in the posts model
  • Add a new categories relation field in the posts model
  • Add the postIds scalar list field in the categories model
  • Add the posts relation in the categories model
  • Add a relation scalar on both models
  • Add the @relation attribute specifying the fields and references arguments on both sides
diff
schema.prisma
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
posts posts[] @relation(fields: [postIds], references: [id])
postIds String[] @db.ObjectId
}
model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean
v Int @map("__v")
author users @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId
categories String[] @db.ObjectId
categories categories[] @relation(fields: [categoryIds], references: [id])
categoryIds String[] @db.ObjectId
}
model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
posts posts[]
}

2.5 Adjust the Prisma schema (optional)

The models that were generated via introspection currently exactly map to your database collections. In this section, you'll learn how you can adjust the naming of the Prisma models to adhere to Prisma's naming conventions.

Some of these adjustments are entirely optional and you are free to skip to the next step already if you don't want to adjust anything for now. You can go back and make the adjustments at any later point.

As opposed to the current snake_case notation of Prisma models, Prisma's naming conventions are:

  • PascalCase for model names
  • camelCase for field names

You can adjust the naming by mapping the Prisma model and field names to the existing table and column names in the underlying database using @@map and @map, respectively.

You can use the rename symbol operation to refactor model names by highlighting the model name, pressing F2, and finally typing the desired name. This will rename all instances where it is referenced and add the @@map() attribute to the existing model with its former name.

If your schema includes a versionKey, update it by adding the @default(0) and @ignore attributes to the v field. This means the field will be excluded from the generated Prisma Client and will have a default value of 0. Prisma does not handle document versioning.

Also note that you can rename relation fields to optimize the Prisma Client API that you'll use later to send queries to your database. For example, the post field on the user model is a list, so a better name for this field would be posts to indicate that it's plural.

Update the published field by including the @default attribute to define the default value of the field.

You can also rename the UserProfile composite type to Profile.

Here's an adjusted version of the Prisma schema that addresses these points:

prisma/schema.prisma
1generator client {
2 provider = "prisma-client-js"
3}
4
5datasource db {
6 provider = "mongodb"
7 url = env("DATABASE_URL")
8}
9
10type Profile {
11 bio String
12}
13
14model Category {
15 id String @id @default(auto()) @map("_id") @db.ObjectId
16 name String
17 v Int @default(0) @map("__v") @ignore
18
19 posts Post[] @relation(fields: [post_ids], references: [id])
20 post_ids String[] @db.ObjectId
21
22 @@map("categories")
23}
24
25model Post {
26 id String @id @default(auto()) @map("_id") @db.ObjectId
27 title String
28 content String
29 published Boolean @default(false)
30 v Int @default(0) @map("__v") @ignore
31
32 author User @relation(fields: [authorId], references: [id])
33 authorId String @map("author") @db.ObjectId
34
35 categories Category[] @relation(fields: [categoryIds], references: [id])
36 categoryIds String[] @db.ObjectId
37
38 @@map("posts")
39}
40
41model User {
42 id String @id @default(auto()) @map("_id") @db.ObjectId
43 v Int @default(0) @map("__v") @ignore
44 email String @unique(map: "email_1")
45 name String
46 profile Profile?
47 posts Post[]
48
49 @@map("users")
50}

Step 3. Install Prisma Client

As a next step, you can install Prisma Client in your project so that you can start replacing the database queries in your project that are currently made with Mongoose:

$npm install @prisma/client

Step 4. Replace your Mongoose queries with Prisma Client

In this section, we'll show a few sample queries that are being migrated from Mongoose to Prisma Client, based on the example routes from the sample REST API project. For a comprehensive overview of how the Prisma Client API differs from Mongoose, check out the Mongoose and Prisma API comparison page.

First, to set up the PrismaClient instance that you'll use to send database queries from the various route handlers, create a new file named prisma.js in the src directory:

$touch src/prisma.js

Now, instantiate PrismaClient and export it from the file so you can use it in your route handlers later:

src/prisma.js
1const { PrismaClient } = require('@prisma/client')
2
3const prisma = new PrismaClient()
4
5module.exports = prisma

The imports in our controller files are as follows:

src/controllers/post.js
1const Post = require('../models/post')
2const User = require('../models/user')
3const Category = require('../models/category')
src/controllers/user.js
1const Post = require('../models/post')
2const User = require('../models/user')

You'll update the controller imports as you migrate from Mongoose to Prisma:

src/controllers/post.js
1const prisma = require('../prisma')
src/controllers/user.js
1const prisma = require('../prisma')

4.1. Replacing queries in GET requests

The example REST API used in this guide has four routes that accept GET requests:

  • /feed?searchString={searchString}&take={take}&skip={skip}: Return all published posts
    • Query Parameters (optional):
      • searchString: Filter posts by title or content
      • take: Specifies how many objects should be returned in the list
      • skip: Specifies how many of the returned objects should be skipped
  • /post/:id: Returns a specific post
  • /authors: Returns a list of authors

Let's dive into the route handlers that implement these requests.

/feed

The /feed handler is implemented as follows:

src/controllers/post.js
1const feed = async (req, res) => {
2 try {
3 const { searchString, skip, take } = req.query
4
5 const or =
6 searchString !== undefined
7 ? {
8 $or: [
9 { title: { $regex: searchString, $options: 'i' } },
10 { content: { $regex: searchString, $options: 'i' } },
11 ],
12 }
13 : {}
14
15 const feed = await Post.find(
16 {
17 ...or,
18 published: true,
19 },
20 null,
21 {
22 skip,
23 batchSize: take,
24 }
25 )
26 .populate({ path: 'author', model: User })
27 .populate('categories')
28
29 return res.status(200).json(feed)
30 } catch (error) {
31 return res.status(500).json(error)
32 }
33}

Note that each returned Post object includes the relation to the author and category with which it is associated. With Mongoose, including the relation is not type-safe. For example, if there was a typo in the relation that is retrieved, your database query would fail only at runtime – the JavaScript compiler does not provide any safety here.

Here is how the same route handler is implemented using Prisma Client:

src/controllers/post.js
1const feed = async (req, res) => {
2 try {
3 const { searchString, skip, take } = req.query
4
5 const or = searchString
6 ? {
7 OR: [
8 { title: { contains: searchString } },
9 { content: { contains: searchString } },
10 ],
11 }
12 : {}
13
14 const feed = await prisma.post.findMany({
15 where: {
16 published: true,
17 ...or,
18 },
19 include: { author: true, categories: true },
20 take: Number(take) || undefined,
21 skip: Number(skip) || undefined,
22 })
23
24 return res.status(200).json(feed)
25 } catch (error) {
26 return res.status(500).json(error)
27 }
28}

Note that the way in which Prisma Client includes the author relation is absolutely type-safe. The JavaScript compiler would throw an error if you were trying to include a relation that does not exist on the Post model.

/post/:id

The /post/:id handler is implemented as follows:

src/controllers/post.js
1const getPostById = async (req, res) => {
2 const { id } = req.params
3
4 try {
5 const post = await Post.findById(id)
6 .populate({ path: 'author', model: User })
7 .populate('categories')
8
9 if (!post) return res.status(404).json({ message: 'Post not found' })
10
11 return res.status(200).json(post)
12 } catch (error) {
13 return res.status(500).json(error)
14 }
15}

With Prisma, the route handler is implemented as follows:

src/controllers/post.js
1const getPostById = async (req, res) => {
2 const { id } = req.params
3
4 try {
5 const post = await prisma.post.findUnique({
6 where: { id },
7 include: {
8 author: true,
9 category: true,
10 },
11 })
12
13 if (!post) return res.status(404).json({ message: 'Post not found' })
14
15 return res.status(200).json(post)
16 } catch (error) {
17 return res.status(500).json(error)
18 }
19}

4.2. Replacing queries in POST requests

The REST API has three routes that accept POST requests:

  • /user: Creates a new User record
  • /post: Creates a new User record
  • /user/:id/profile: Creates a new Profile record for a User record with a given ID

/user

The /user handler is implemented as follows:

src/controllers/user.js
1const createUser = async (req, res) => {
2 const { name, email } = req.body
3
4 try {
5 const user = await User.create({
6 name,
7 email,
8 })
9
10 return res.status(201).json(user)
11 } catch (error) {
12 return res.status(500).json(error)
13 }
14}

With Prisma, the route handler is implemented as follows:

src/controllers/user.js
1const createUser = async (req, res) => {
2 const { name, email } = req.body
3
4 try {
5 const user = await prisma.user.create({
6 data: {
7 name,
8 email,
9 },
10 })
11
12 return res.status(201).json(user)
13 } catch (error) {
14 return res.status(500).json(error)
15 }
16}

/post

The /post handler is implemented as follows:

src/controllers/post.js
1const createDraft = async (req, res) => {
2 const { title, content, authorEmail } = req.body
3
4 try {
5 const author = await User.findOne({ email: authorEmail })
6
7 if (!author) return res.status(404).json({ message: 'Author not found' })
8
9 const draft = await Post.create({
10 title,
11 content,
12 author: author._id,
13 })
14
15 res.status(201).json(draft)
16 } catch (error) {
17 return res.status(500).json(error)
18 }
19}

With Prisma, the route handler is implemented as follows:

src/controllers/post.js
1const createDraft = async (req, res) => {
2 const { title, content, authorEmail } = req.body
3
4 try {
5 const draft = await prisma.post.create({
6 data: {
7 title,
8 content,
9 author: {
10 connect: {
11 email: authorEmail,
12 },
13 },
14 },
15 })
16
17 res.status(201).json(draft)
18 } catch (error) {
19 return res.status(500).json(error)
20 }
21}

Note that Prisma Client's nested write here saves the initial query where the User record is first retrieved by its email. That's because, with Prisma you can connect records in relations using any unique property.

/user/:id/profile

The /user/:id/profile handler is implemented as follows:

src/controllers/user.js
1const setUserBio = async (req, res) => {
2 const { id } = req.params
3 const { bio } = req.body
4
5 try {
6 const user = await User.findByIdAndUpdate(
7 id,
8 {
9 profile: {
10 bio,
11 },
12 },
13 { new: true }
14 )
15
16 if (!user) return res.status(404).json({ message: 'Author not found' })
17
18 return res.status(200).json(user)
19 } catch (error) {
20 return res.status(500).json(error)
21 }
22}

With Prisma, the route handler is implemented as follows:

src/controllers/user.js
1const setUserBio = async (req, res) => {
2 const { id } = req.params
3 const { bio } = req.body
4
5 try {
6 const user = await prisma.user.update({
7 where: { id },
8 data: {
9 profile: {
10 bio,
11 },
12 },
13 })
14
15 if (!user) return res.status(404).json({ message: 'Author not found' })
16
17 return res.status(200).json(user)
18 } catch (error) {
19 console.log(error)
20 return res.status(500).json(error)
21 }
22}

Alternatively, you can use the set property to update the value of an embedded document as follows:

src/controllers/user.js
1const setUserBio = async (req, res) => {
2 const { id } = req.params
3 const { bio } = req.body
4
5 try {
6 const user = await prisma.user.update({
7 where: {
8 id,
9 },
10 data: {
11 profile: {
12 set: { bio },
13 },
14 },
15 })
16
17 return res.status(200).json(user)
18 } catch (error) {
19 console.log(error)
20 return res.status(500).json(error)
21 }
22}

4.3. Replacing queries in PUT requests

The REST API has two routes that accept a PUT request:

  • /post/:id/:categoryId: Adds the post with :id to the category with :categoryId
  • /post/:id: Updates the published status of a post to true.

Let's dive into the route handlers that implement these requests.

/post/:id/:categoryId

The /post/:id/:categoryId handler is implemented as follows:

src/controllers/post.js
1const addPostToCategory = async (req, res) => {
2 const { id, categoryId } = req.params
3
4 try {
5 const category = await Category.findById(categoryId)
6
7 if (!category)
8 return res.status(404).json({ message: 'Category not found' })
9
10 const post = await Post.findByIdAndUpdate(
11 { _id: id },
12 {
13 categories: [{ _id: categoryId }],
14 },
15 { new: true }
16 )
17
18 if (!post) return res.status(404).json({ message: 'Post not found' })
19 return res.status(200).json(post)
20 } catch (error) {
21 return res.status(500).json(error)
22 }
23}

With Prisma, the handler is implemented as follows:

src/controllers/post.js
1const addPostToCategory = async (req, res) => {
2 const { id, categoryId } = req.query
3
4 try {
5 const post = await prisma.post.update({
6 where: {
7 id,
8 },
9 data: {
10 categories: {
11 connect: {
12 id: categoryId,
13 },
14 },
15 },
16 })
17
18 if (!post) return res.status(404).json({ message: 'Post not found' })
19
20 return res.status(200).json(post)
21 } catch (error) {
22 console.log({ error })
23 return res.status(500).json(error)
24 }
25}

/post/:id

The /post/:id handler is implemented as follows:

src/controllers/post.js
1const publishDraft = async (req, res) => {
2 const { id } = req.params
3
4 try {
5 const post = await Post.findByIdAndUpdate(
6 { id },
7 { published: true },
8 { new: true }
9 )
10 return res.status(200).json(post)
11 } catch (error) {
12 return res.status(500).json(error)
13 }
14}

With Prisma, the handler is implemented as follows:

src/controllers/post.js
1const publishDraft = async (req, res) => {
2 const { id } = req.params
3
4 try {
5 const post = await prisma.post.update({
6 where: { id },
7 data: { published: true },
8 })
9 return res.status(200).json(post)
10 } catch (error) {
11 return res.status(500).json(error)
12 }
13}

More

Embedded documents _id field

By default, Mongoose assigns each document and embedded document an _id field. If you wish to disable this option for embedded documents, you can set the _id option to false.

const ProfileSchema = new Schema(
{
bio: String,
},
{
_id: false,
}
)

Document version key

Mongoose assigns each document a version when created. You can disable Mongoose from versioning your documents by setting the versionKey option of a model to false. It is not recommended to disable this unless you are an advanced user.

const ProfileSchema = new Schema(
{
bio: String,
},
{
versionKey: false,
}
)

When migrating to Prisma, mark the versionKey field as optional ( ? ) in your Prisma schema and add the @ignore attribute to exclude it from Prisma Client.

Collection name inference

Mongoose infers the collection names by automatically converting the model names to lowercase and plural form.

On the other hand, Prisma maps the model name to the table name in your database modeling your data.

You can enforce the collection name in Mongoose to have the same name as the model by setting the option while creating your schema

const PostSchema = new Schema(
{
title: String,
content: String,
// more fields here
},
{
collection: 'Post',
}
)

Modeling relations

You can model relations in Mongoose between documents by either using sub-documents or storing a reference to other documents.

Prisma allows you to model different types of relations between documents when working with MongoDB:

Edit this page on GitHub