Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

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 .md to 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.

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.

prisma
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.

TypeScript
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.

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():

prisma
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.

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.

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.

prisma
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:

TypeScript
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:

TypeScript
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:

TypeScript
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.

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:

prisma
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:

TypeScript
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.

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.

prisma
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:

TypeScript
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.

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."
  • Start a new project with npm create prisma@latest -- my-app, or run npx prisma orm init in an existing one. After every change to your contract, run npx prisma contract emit to check it and regenerate your types, then npx prisma migration plan to write a migration and npx prisma db migrate to 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.
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu