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:
- Install the Prisma CLI
- Introspect your database
- Install 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 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):
const mongoose = require('mongoose')const Schema = mongoose.Schemaconst 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:
$
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:
$
This command creates:
- A new directory called
prisma
that contains aschema.prisma
file; your Prisma schema file 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 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-schema34datasource db {5 provider = "mongodb"6 url = env("DATABASE_URL")7}89generator 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.
$
This creates the following Prisma models:
prisma/schema.prisma
1type UsersProfile {2 bio String3}45model categories {6 id String @id @default(auto()) @map("_id") @db.ObjectId7 v Int @map("__v")8 name String9}1011model posts {12 id String @id @default(auto()) @map("_id") @db.ObjectId13 v Int @map("__v")14 author String @db.ObjectId15 categories String[] @db.ObjectId16 content String17 published Boolean18 title String19}2021model users {22 id String @id @default(auto()) @map("_id") @db.ObjectId23 v Int @map("__v")24 email String @unique(map: "email_1")25 name String26 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 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
type UsersProfile {bio String}model categories {id String @id @default(auto()) @map("_id") @db.ObjectIdv Int @map("__v")name String}model posts {id String @id @default(auto()) @map("_id") @db.ObjectIdtitle Stringcontent Stringpublished Booleanv Int @map("__v")author String @db.ObjectIdauthor users @relation(fields: [authorId], references: [id])authorId String @map("author") @db.ObjectIdcategories String[] @db.ObjectId}model users {id String @id @default(auto()) @map("_id") @db.ObjectIdv Int @map("__v")email String @unique(map: "email_1")name Stringprofile 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
type UsersProfile {bio String}model categories {id String @id @default(auto()) @map("_id") @db.ObjectIdv Int @map("__v")name Stringposts posts[] @relation(fields: [postIds], references: [id])postIds String[] @db.ObjectId}model posts {id String @id @default(auto()) @map("_id") @db.ObjectIdtitle Stringcontent Stringpublished Booleanv Int @map("__v")author users @relation(fields: [authorId], references: [id])authorId String @map("author") @db.ObjectIdcategories String[] @db.ObjectIdcategories categories[] @relation(fields: [categoryIds], references: [id])categoryIds String[] @map("categories") @db.ObjectId}model users {id String @id @default(auto()) @map("_id") @db.ObjectIdv Int @map("__v")email String @unique(map: "email_1")name Stringprofile 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}45datasource db {6 provider = "mongodb"7 url = env("DATABASE_URL")8}910type Profile {11 bio String12}1314model Category {15 id String @id @default(auto()) @map("_id") @db.ObjectId16 name String17 v Int @default(0) @map("__v") @ignore1819 posts Post[] @relation(fields: [post_ids], references: [id])20 post_ids String[] @db.ObjectId2122 @@map("categories")23}2425model Post {26 id String @id @default(auto()) @map("_id") @db.ObjectId27 title String28 content String29 published Boolean @default(false)30 v Int @default(0) @map("__v") @ignore3132 author User @relation(fields: [authorId], references: [id])33 authorId String @map("author") @db.ObjectId3435 categories Category[] @relation(fields: [categoryIds], references: [id])36 categoryIds String[] @db.ObjectId3738 @@map("posts")39}4041model User {42 id String @id @default(auto()) @map("_id") @db.ObjectId43 v Int @default(0) @map("__v") @ignore44 email String @unique(map: "email_1")45 name String46 profile Profile?47 posts Post[]4849 @@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:
$
Now, instantiate PrismaClient
and export it from the file so you can use it in your route handlers later:
src/prisma.js
12345
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 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:
src/controllers/post.js
1const feed = async (req, res) => {2 try {3 const { searchString, skip, take } = req.query45 const or =6 searchString !== undefined7 ? {8 $or: [9 { title: { $regex: searchString, $options: 'i' } },10 { content: { $regex: searchString, $options: 'i' } },11 ],12 }13 : {}1415 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')2829 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.query45 const or = searchString6 ? {7 OR: [8 { title: { contains: searchString } },9 { content: { contains: searchString } },10 ],11 }12 : {}1314 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 })2324 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.params34 try {5 const post = await Post.findById(id)6 .populate({ path: 'author', model: User })7 .populate('categories')89 if (!post) return res.status(404).json({ message: 'Post not found' })1011 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.params34 try {5 const post = await prisma.post.findUnique({6 where: { id },7 include: {8 author: true,9 category: true,10 },11 })1213 if (!post) return res.status(404).json({ message: 'Post not found' })1415 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 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:
src/controllers/user.js
1const createUser = async (req, res) => {2 const { name, email } = req.body34 try {5 const user = await User.create({6 name,7 email,8 })910 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.body34 try {5 const user = await prisma.user.create({6 data: {7 name,8 email,9 },10 })1112 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.body34 try {5 const author = await User.findOne({ email: authorEmail })67 if (!author) return res.status(404).json({ message: 'Author not found' })89 const draft = await Post.create({10 title,11 content,12 author: author._id,13 })1415 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.body34 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 })1617 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.params3 const { bio } = req.body45 try {6 const user = await User.findByIdAndUpdate(7 id,8 {9 profile: {10 bio,11 },12 },13 { new: true }14 )1516 if (!user) return res.status(404).json({ message: 'Author not found' })1718 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.params3 const { bio } = req.body45 try {6 const user = await prisma.user.update({7 where: { id },8 data: {9 profile: {10 bio,11 },12 },13 })1415 if (!user) return res.status(404).json({ message: 'Author not found' })1617 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.params3 const { bio } = req.body45 try {6 const user = await prisma.user.update({7 where: {8 id,9 },10 data: {11 profile: {12 set: { bio },13 },14 },15 })1617 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 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:
src/controllers/post.js
1const addPostToCategory = async (req, res) => {2 const { id, categoryId } = req.params34 try {5 const category = await Category.findById(categoryId)67 if (!category)8 return res.status(404).json({ message: 'Category not found' })910 const post = await Post.findByIdAndUpdate(11 { _id: id },12 {13 categories: [{ _id: categoryId }],14 },15 { new: true }16 )1718 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.query34 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 })1718 if (!post) return res.status(404).json({ message: 'Post not found' })1920 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.params34 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.params34 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: