MongoDB data modeling (/docs/orm/data-modeling/mongodb)
For the complete Prisma documentation index, see llms.txt. A markdown version of any docs page is available by appending
.mdto its URL.
Model documents, embedded documents, and references for MongoDB, and decide when to embed and when to reference.
Location: ORM > Data modeling > MongoDB data modeling
MongoDB stores data as documents grouped into collections. A document can nest objects and arrays directly, which gives you a real choice for related data: keep it inside the document (embed it) or store it in its own collection and link to it (reference it). Making that choice well is the core of MongoDB data modeling. If you are new to models and keys, start with the data modeling overview.
Documents, collections, and the _id key
Section titled “Documents, collections, and the _id key”Your models go in your contract, the contract.prisma file that replaced schema.prisma. A model maps to a collection, each record is a document, and every document has an _id field as its primary key. There is no datasource block on MongoDB: your connection string goes in prisma.config.ts instead, passed to defineConfig from @prisma/orm-mongo/config, and the data contract page shows that config file.
model User {
id ObjectId @id @map("_id")
name String
email String @unique
@@map("users")
}In the contract you call the field id, but in TypeScript it is always _id, never id, because @map("_id") sets the name in the database and in your code alike. So you write user._id and where({ _id }). @@map("users") names the collection and is optional: without it the collection takes the model name exactly as written, so User. @unique on email becomes a unique index when you migrate.
Write ObjectId for the key type. That is the contract type only: in TypeScript a document's own _id is a plain string, so there is no ObjectId to construct. The old String @db.ObjectId and @default(auto()) are no longer allowed.
Leave the id out of the values you pass to create, and MongoDB assigns it. In the examples below, db is the client created in src/prisma/db.ts, and db.orm is the ORM client, one of the query APIs on db that the ORM reference covers.
import { db } from "./prisma/db";
const user = await db.orm.users.create({ name: "Ada", email: "ada@example.com" });
const found = await db.orm.users.where({ _id: user._id }).first();The name after db.orm. is the collection name, so with @@map("users") you write db.orm.users, not db.orm.user. The values go straight into create, without the data: wrapper Prisma ORM 7 used. You await the whole chain, and the last call says what you want back: .first() for one document or null, .all() for every matching document.
Field types
Section titled “Field types”On MongoDB, each scalar type is named after the BSON type it stores:
| Type | Stored as | In your code |
|---|---|---|
String |
BSON string | string |
Int32 |
BSON int | number |
Int64 |
BSON long | bigint |
Double |
BSON double | number |
Decimal128 |
BSON decimal | a string, so that no digit is lost |
Bool |
BSON bool | boolean |
Date |
BSON date | a JavaScript Date |
Binary |
BSON binary | Uint8Array |
Json |
a BSON object, array, string, number, bool or null | a JSON value |
Bson |
any BSON value | the value the MongoDB driver returns |
ObjectId |
BSON ObjectId | string |
A Json field holds only JSON values. A read fails with RUNTIME.DECODE_FAILED, and a write with RUNTIME.ENCODE_FAILED, when the field holds a Date, an ObjectId, a Decimal128, a Binary or another value that is not JSON, at any depth. Type the field Bson when it holds such values. A contract whose source is a Prisma ORM 6 schema cannot declare Bson.
The names Int, Float, Boolean and DateTime from earlier releases are deprecated. They still work and give the same contract, but contract emit reports a PSL_DEPRECATED_SCALAR_NAME warning for each use, and a later release removes them. Rename them to Int32, Double, Bool and Date.
For a timestamp that Prisma ORM fills in, type the field temporal.createdAt() or temporal.updatedAt():
model Post {
id ObjectId @id @map("_id")
title String
createdAt temporal.createdAt()
updatedAt temporal.updatedAt()
@@map("posts")
}With a client created by mongo(), create sets both fields, and every update that changes something sets updatedAt again. Both fields are optional in the values you pass to create.
Embed or reference
Section titled “Embed or reference”Two pieces of related data can live together in one document (embedded) or in separate collections linked by an id (referenced). Embedding nests the data inside its parent: an order and its line items in one document. One read returns everything, and a write updates parent and children together, atomically. The trade-off is that you cannot read embedded data without its parent, and it comes back on every read of the parent.
Referencing keeps the data in its own collection and stores the linked document's _id. Loading both takes a second read, but each record stands on its own: it can be queried, listed, and updated independently, and it is not duplicated when several parents point at it. A quick way to decide:
| Signal | Example | Lean toward |
|---|---|---|
| Always loaded with the parent | An order's line items | Embed |
| Small and bounded in size | A user's mailing address | Embed |
| No meaning outside the parent | A post's SEO metadata | Embed |
| Grows without limit | A user's activity events | Reference |
| Queried or updated on its own | Products in a catalog | Reference |
| Shared by many parents | A tag on thousands of posts | Reference |
A blog post's comments make it concrete. If you always render them with the post and their count stays modest, embed them. If comments can grow into the thousands, or you need "every comment by this author across posts", reference them.
Embedded documents
Section titled “Embedded documents”Describe the shape of embedded data with a type block, then use it as a field. An embedded value has no _id and no collection of its own, so it is created, loaded, updated, and deleted with the document that holds it.
type Address {
street String
city String
country String
}
model User {
id ObjectId @id @map("_id")
name String
tags String[]
address Address?
@@map("users")
}A single embedded value models a one-to-one relation. A list of embedded values models a one-to-many relation: addresses Address[] stores every address inside the user document, with no second collection and no join to load them. Pass an embedded value to create as a plain nested object:
const address = { street: "1 Main St", city: "Berlin", country: "DE" };
await db.orm.users.create({ name: "Ada", tags: [], address });In an update, the callback argument u builds the changes you want, and because the callback returns an array, several changes go in one call. Use u.fieldName to change a top-level field, or u("path.to.field") for a field inside an embedded document, written as a string because a dotted path is not a property name. On a list of scalars, .push(...) adds values and .pull(...) removes them:
await db.orm.users.where({ _id: user._id }).update((u) => [u("address.city").set("Hamburg")]);
await db.orm.users.where({ _id: user._id }).update((u) => [u.tags.push("admin"), u.tags.pull("new")]);Replace a whole embedded value with u.address.set({ ... }), remove it with u.address.unset(), and replace a list of embedded values as a whole with u.addresses.set([ ... ]). Filtering is more limited: the object form of .where() filters on top-level model fields only, so to match on a field inside an embedded document you use the pipeline builder, a chained builder for MongoDB aggregation pipelines:
const query = db.query.from("users").match((f) => f("address.city").eq("Berlin")).build();build() returns the query rather than the rows, and the pipeline builder page explains how to run it and what comes back.
Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit, where a write that exceeds it fails, so when a list has no natural cap, reference it instead.
References across collections
Section titled “References across collections”When related records need their own collection, store one document's _id on the other and declare the relation with the same @relation(fields:, references:) used everywhere in Prisma ORM:
model User {
id ObjectId @id @map("_id")
name String
posts Post[]
@@map("users")
}
model Post {
id ObjectId @id @map("_id")
title String
authorId ObjectId
author User @relation(fields: [authorId], references: [id])
@@map("posts")
}This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. Note that in references: [id] you write the field's contract-side name, id, not _id. There are no nested writes on MongoDB, so create the user first and then the post with its authorId. To load a post together with its author, chain .include("author"), which adds the related record to every row you get back:
await db.orm.posts.create({ title: "Hello", authorId: user._id });
const posts = await db.orm.posts.include("author").all();.include("posts") on db.orm.users works the other way and gives each user an array of their posts. See relations and joins for the rest of the query API.
Resolving a reference means a lookup into the other collection on every read. If a read is frequent, you can copy the few fields it needs into the parent document, such as a comment storing its author's name, but the copy does not follow the original when it changes, so you update it yourself.
Polymorphic collections
Section titled “Polymorphic collections”One collection can hold more than one kind of document. You declare a base model for the shared fields and a variant model for each kind, and a discriminator field on the base model records which kind each document is.
model Post {
id ObjectId @id @map("_id")
title String
kind String
@@discriminator(kind)
@@map("posts")
}
model Article {
summary String
@@base(Post, "article")
}
model Tutorial {
difficulty String
duration Int32
@@base(Post, "tutorial")
}@@discriminator(kind) takes the name of the field that records each document's variant, so write kind and not "kind". @@base(Post, "article") declares Article as the "article" variant of Post. A variant declares only its own fields, since id, title, and kind come from Post, and it uses its base's collection, so it needs no @@map of its own.
A query for posts returns articles and tutorials together. To reach a single variant, go through the base model: db.orm.posts.variant("Article") limits the query to one variant, and it takes the variant's model name, "Article", not the discriminator value "article". Creating through a variant fills in kind for you:
const article = await db.orm.posts.variant("Article").create({ title: "Hello", summary: "A short post" });Use one collection when you almost always query the variants together, for example email, SMS, and push notifications that you read as a single feed. Prefer separate collections when the types rarely appear in the same query or need very different indexes.
Prompt your coding agent
Section titled “Prompt your coding agent”Projects created with npm create prisma@latest include the Prisma ORM skills, instruction files for coding agents. In an existing project, run npx prisma skills sync to add them. The prisma-8 skill covers document modeling, so try prompts that map to each section:
- "Using the prisma-8 skill, add an embedded Address type to the User model."
- "Cart items are always read with the cart. Model them as an embedded list."
- "Comments can grow without limit. Move them from an embedded array to a referenced collection."
- "Split the posts collection into Article and Tutorial variants with a discriminator."
Next steps
Section titled “Next steps”- Start a new project with
npm create prisma@latest -- my-app, or runnpx prisma orm initin an existing one. After every change to your contract, runnpx prisma contract emitto check it and regenerate your types, thennpx prisma migration planto write a migration andnpx prisma db migrateto apply it, which creates the collections and indexes. Generating a migration has the flags and explains how the next plan finds its starting point. - Query documents and resolve references with
.include(...). - Aggregate and reshape documents with the pipeline builder.
- To model relational data, see the relational data modeling guide.
Related pages
Section titled “Related pages”Relational data modeling: Model one-to-one, one-to-many, many-to-many, and polymorphic relations for PostgreSQL.