Data Modeling
Personal Catalogue: React, Express & MongoDB
Chapter 2 · Data Modeling
Chapter 1 promised a real, working answer to the shared four-course spec's own genuinely heterogeneous item data — one MongoDB collection, discriminated by item type, built with Mongoose's own real discriminator mechanism rather than a vague "just store whatever JSON shows up" approach. This chapter builds it.
What the Relational Siblings Chose, and Why This One Diverges
The PHP & MySQL sibling's own Chapter 2 settles on a deliberately generic, shared items table —
one row per item regardless of type, with columns like creator (Author/Artist/Director depending
on type) and format_detail (Hardcover-paperback / Single-album / Region code) standing in for
whatever that item type's own equivalent field actually is, plus a separate item_types lookup
table and a tags/item_tags many-to-many join table. That design works specifically
because relational storage forces one shared column set up front — genericizing the columns is the
real workaround for a rigid schema, not a design goal in its own right.
MongoDB's document model doesn't have that constraint, so this course doesn't need that workaround either.
Instead of one generic creator field standing in for four different real concepts, a Book
document gets a real author field, a Cd document gets a real artist field, and a Dvd
or Bluray document gets a real director field — each one named for what it actually is, not for
what every other item type also happens to have.
The Base Item Schema
Every discriminator setup starts with a base schema holding the fields genuinely shared across all four item
types — title, an optional releaseYear, and freeform notes — plus the
discriminatorKey option that names the field Mongoose will use to record which subtype a given
document actually is:
discriminatorKey is left unset, Mongoose falls back to a default field called __t.
Naming it itemType here is a deliberate choice for readability, not a requirement — the
mechanism works identically either way, it's just genuinely more obvious what a stored document's own
itemType: "Book" field means than a bare __t: "Book" would be.
Building the Four Discriminators
Item.discriminator(name, schema) registers a subtype schema against the base model. Every field
defined on itemSchema is still present on the resulting documents — a discriminator schema only
adds fields, it doesn't replace anything. Book gets the fields no other item type needs, including
the tags array this project's own spec restricts to books specifically:
DVDs and Blu-rays share the most fields of any two types in this spec — both have a director and a runtime — but they're still registered as two genuinely separate discriminators rather than one, since a Blu-ray's own resolution and a DVD's own region code aren't the same real-world attribute wearing a different name:
One Collection, Four Document Shapes
Every document created through Book, Cd, Dvd, or Bluray
is written into the exact same underlying MongoDB collection — items, per the
collection option set on the base schema — with the itemType field recording which
discriminator produced it:
Querying through the base Item model returns documents of every type mixed together — exactly
the "do I already own this?" search this project's own spec needs, resolved as a single query rather than a
four-way union. Querying through a specific discriminator model like Book filters automatically
to just that type, with no manual itemType: "Book" condition needed in the query itself.
Item model doesn't expose author,
artist, or any other discriminator-specific field, even though the raw MongoDB document actually
has that data stored on it — Mongoose only attaches the extra schema fields when the document is loaded
through the matching discriminator model. Chapter 6's own cross-type search route works around this directly
by reading the raw document data rather than relying on every field being present on a base-model instance.
Trying It End to End
A quick script confirms all four discriminators write into, and can be read back out of, the same real collection:
Running this against a real MongoDB instance and then inspecting the collection directly (via
mongosh or a GUI like MongoDB Compass) shows exactly one document, in exactly one collection,
carrying its own real itemType: "Book" field alongside the base schema's shared fields.
Cd.create({ title: 'x' })
with no artist field fails with a real Mongoose validation error, the exact same way a missing
NOT NULL column would fail on the PHP sibling's relational schema, even though every item type
still lives in one shared, flexible collection.
Hands-On Exercises
Define all four discriminator schemas (Book, Cd, Dvd, Bluray) against the shared Item base schema, create one real document of each type, then query through the base Item model and confirm all four documents come back from a single query against a single collection.
📄 View solutionExplain why this course chose Mongoose discriminators over four separate MongoDB collections (one per item type), and name the one real feature that choice specifically simplifies.
📄 View solutionWrite a real Mongoose query that creates a Book document with at least two tags, then a second query demonstrating tag-based filtering — finding only Book documents that carry one specific tag.
📄 View solutionChapter 2 Quick Reference
- Base schema —
title/releaseYear/notes, plusdiscriminatorKey: 'itemType' - Four discriminators —
Item.discriminator('Book'|'Cd'|'Dvd'|'Bluray', schema), each adding genuinely type-specific fields - One collection — every discriminator writes into the same underlying
itemscollection, distinguished by the storeditemTypefield - Tags — a plain
[String]array, present only on the Book discriminator schema - Real validation — each discriminator still enforces its own required fields on write, even inside one shared collection
- Known limitation — a document loaded through the base
Itemmodel doesn't expose subtype fields; load through the matching discriminator model instead - Next chapter: Building the Express API — real CRUD routes for every item type