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 to 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
prisma
that contains aschema.prisma
file; your Prisma schema specifies your database connection and models .env
: Adotenv
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 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
author
reference in theposts
model toauthorId
and add the@map("author")
attribute - Add the
author
relation field in theposts
model and it's@relation
attribute specifying thefields
andreferences
- Add the
posts
relation in theusers
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[]
}
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
categories
field tocategoryIds
and map it using@map("categories")
in theposts
model - Add a new
categories
relation field in theposts
model - Add the
postIds
scalar list field in thecategories
model - Add the
posts
relation in thecategories
model - Add a relation scalar on both models
- Add the
@relation
attribute specifying thefields
andreferences
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[] @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 bytitle
orcontent
take
: 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 newUser
record/post
: Creates a newUser
record/user/:id/profile
: Creates a newProfile
record for aUser
record 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:id
to the category with:categoryId
/post/:id
: Updates thepublished
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:
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: