Migrate from Mongoose
This guide describes how to migrate from Mongoose to Prisma ORM. 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 ORM compares to Mongoose on the Prisma ORM vs Mongoose page.
Overview of the migration process
Note that the steps for migrating from Mongoose to Prisma ORM are always the same, no matter what kind of application or API layer you're building:
- Install the Prisma CLI
- Introspect your database
- Install and generate Prisma Client
- 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 ORM lends itself really well for incremental adoption. This means, you don't have migrate your entire project from Mongoose to Prisma ORM at once, but rather you can step-by-step move your database queries from Mongoose to Prisma ORM.
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 ORM. 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)
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ProfileSchema = new Schema(
{
bio: String,
},
{
_id: false,
}
)
const UserSchema = new Schema({
name: String,
email: {
type: String,
unique: true,
},
profile: {
type: ProfileSchema,
default: () => ({}),
},
})
module.exports = mongoose.model('User', UserSchema)
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const CategorySchema = new Schema({
name: {
type: String,
required: true,
},
})
module.exports = mongoose.model('Category', CategorySchema)
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 ORM 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 ORM 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 ORM 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
prismathat contains aschema.prismafile; your Prisma schema file specifies your database connection and models .env: Adotenvfile 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:
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
For an optimal development experience when working with Prisma ORM, 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 ORM uses.
DATABASE_URL="mongodb://alice:myPassword43@localhost:27017/blog-mongoose"
Refer to the MongoDB connection URL specification for further details.
2.3. Run Prisma ORM'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 ORM in your project, ensure your database is populated with sample data. Prisma ORM 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:
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
v Int @map("__v")
author String @db.ObjectId
categories String[] @db.ObjectId
content String
published Boolean
title String
}
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?
}
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
authorreference in thepostsmodel toauthorIdand add the@map("author")attribute - Add the
authorrelation field in thepostsmodel and it's@relationattribute specifying thefieldsandreferences - Add the
postsrelation in theusersmodel
- 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[]
}
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 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
categoriesfield tocategoryIdsand map it using@map("categories")in thepostsmodel - Add a new
categoriesrelation field in thepostsmodel - Add the
postIdsscalar list field in thecategoriesmodel - Add the
postsrelation in thecategoriesmodel - Add a relation scalar on both models
- Add the
@relationattribute specifying thefieldsandreferencesarguments 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[] @map("categories") @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[]
}
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
v Int @map("__v")
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 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 ORM'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 ORM'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 ORM 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:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
type Profile {
bio String
}
model Category {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
v Int @default(0) @map("__v") @ignore
posts Post[] @relation(fields: [post_ids], references: [id])
post_ids String[] @db.ObjectId
@@map("categories")
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean @default(false)
v Int @default(0) @map("__v") @ignore
author User @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId
categories Category[] @relation(fields: [categoryIds], references: [id])
categoryIds String[] @db.ObjectId
@@map("posts")
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @default(0) @map("__v") @ignore
email String @unique(map: "email_1")
name String
profile Profile?
posts Post[]
@@map("users")
}
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:
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
module.exports = prisma
The imports in our controller files are as follows:
const Post = require('../models/post')
const User = require('../models/user')
const Category = require('../models/category')
const Post = require('../models/post')
const User = require('../models/user')
You'll update the controller imports as you migrate from Mongoose to Prisma:
const prisma = require('../prisma')
const 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 bytitleorcontenttake: Specifies how many objects should be returned in the listskip: Specifies how many of the returned objects should be skipped
- Query Parameters (optional):
/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:
const feed = async (req, res) => {
try {
const { searchString, skip, take } = req.query
const or =
searchString !== undefined
? {
$or: [
{ title: { $regex: searchString, $options: 'i' } },
{ content: { $regex: searchString, $options: 'i' } },
],
}
: {}
const feed = await Post.find(
{
...or,
published: true,
},
null,
{
skip,
batchSize: take,
}
)
.populate({ path: 'author', model: User })
.populate('categories')
return res.status(200).json(feed)
} catch (error) {
return res.status(500).json(error)
}
}
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:
const feed = async (req, res) => {
try {
const { searchString, skip, take } = req.query
const or = searchString
? {
OR: [
{ title: { contains: searchString } },
{ content: { contains: searchString } },
],
}
: {}
const feed = await prisma.post.findMany({
where: {
published: true,
...or,
},
include: { author: true, categories: true },
take: Number(take) || undefined,
skip: Number(skip) || undefined,
})
return res.status(200).json(feed)
} catch (error) {
return res.status(500).json(error)
}
}
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:
const getPostById = async (req, res) => {
const { id } = req.params
try {
const post = await Post.findById(id)
.populate({ path: 'author', model: User })
.populate('categories')
if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
With Prisma ORM, the route handler is implemented as follows:
const getPostById = async (req, res) => {
const { id } = req.params
try {
const post = await prisma.post.findUnique({
where: { id },
include: {
author: true,
category: true,
},
})
if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
4.2. Replacing queries in POST requests
The REST API has three routes that accept POST requests:
/user: Creates a newUserrecord/post: Creates a newUserrecord/user/:id/profile: Creates a newProfilerecord for aUserrecord with a given ID
/user
The /user handler is implemented as follows:
const createUser = async (req, res) => {
const { name, email } = req.body
try {
const user = await User.create({
name,
email,
})
return res.status(201).json(user)
} catch (error) {
return res.status(500).json(error)
}
}
With Prisma ORM, the route handler is implemented as follows:
const createUser = async (req, res) => {
const { name, email } = req.body
try {
const user = await prisma.user.create({
data: {
name,
email,
},
})
return res.status(201).json(user)
} catch (error) {
return res.status(500).json(error)
}
}
/post
The /post handler is implemented as follows:
const createDraft = async (req, res) => {
const { title, content, authorEmail } = req.body
try {
const author = await User.findOne({ email: authorEmail })
if (!author) return res.status(404).json({ message: 'Author not found' })
const draft = await Post.create({
title,
content,
author: author._id,
})
res.status(201).json(draft)
} catch (error) {
return res.status(500).json(error)
}
}
With Prisma ORM, the route handler is implemented as follows:
const createDraft = async (req, res) => {
const { title, content, authorEmail } = req.body
try {
const draft = await prisma.post.create({
data: {
title,
content,
author: {
connect: {
email: authorEmail,
},
},
},
})
res.status(201).json(draft)
} catch (error) {
return res.status(500).json(error)
}
}
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 Client you can connect records in relations using any unique property.
/user/:id/profile
The /user/:id/profile handler is implemented as follows:
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body
try {
const user = await User.findByIdAndUpdate(
id,
{
profile: {
bio,
},
},
{ new: true }
)
if (!user) return res.status(404).json({ message: 'Author not found' })
return res.status(200).json(user)
} catch (error) {
return res.status(500).json(error)
}
}
With Prisma ORM, the route handler is implemented as follows:
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body
try {
const user = await prisma.user.update({
where: { id },
data: {
profile: {
bio,
},
},
})
if (!user) return res.status(404).json({ message: 'Author not found' })
return res.status(200).json(user)
} catch (error) {
console.log(error)
return res.status(500).json(error)
}
}
Alternatively, you can use the set property to update the value of an embedded document as follows:
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body
try {
const user = await prisma.user.update({
where: {
id,
},
data: {
profile: {
set: { bio },
},
},
})
return res.status(200).json(user)
} catch (error) {
console.log(error)
return res.status(500).json(error)
}
}
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:idto the category with:categoryId/post/:id: Updates thepublishedstatus 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:
const addPostToCategory = async (req, res) => {
const { id, categoryId } = req.params
try {
const category = await Category.findById(categoryId)
if (!category)
return res.status(404).json({ message: 'Category not found' })
const post = await Post.findByIdAndUpdate(
{ _id: id },
{
categories: [{ _id: categoryId }],
},
{ new: true }
)
if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
With Prisma ORM, the handler is implemented as follows:
const addPostToCategory = async (req, res) => {
const { id, categoryId } = req.query
try {
const post = await prisma.post.update({
where: {
id,
},
data: {
categories: {
connect: {
id: categoryId,
},
},
},
})
if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
console.log({ error })
return res.status(500).json(error)
}
}
/post/:id
The /post/:id handler is implemented as follows:
const publishDraft = async (req, res) => {
const { id } = req.params
try {
const post = await Post.findByIdAndUpdate(
{ id },
{ published: true },
{ new: true }
)
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
With Prisma ORM, the handler is implemented as follows:
const publishDraft = async (req, res) => {
const { id } = req.params
try {
const post = await prisma.post.update({
where: { id },
data: { published: true },
})
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
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 ORM, 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 ORM 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 ORM allows you to model different types of relations between documents when working with MongoDB: