Data Modeling
Personal Catalogue: React & Firebase
Chapter 2 · Data Modeling
Chapter 1 promised two things for this chapter: the real Firestore document shapes, built to sit side by side with the MongoDB sibling's own schema for direct comparison, and a real answer to the validation gap that comparison exposed. This chapter delivers both — though not every piece of the second one lands here; where the resulting validation actually gets called from is still an open architectural question, resolved for real in Chapter 3.
What the MongoDB Sibling Built
Personal Catalogue (React, Express & MongoDB)'s own Chapter 2 built a base Item schema
(title, releaseYear, notes) and four Mongoose discriminators
— Book, Cd, Dvd, Bluray — each adding real, type-specific
fields on top of that shared base, all writing into one physical items collection distinguished
by a stored itemType field. Every discriminator schema still enforces its own required fields at
write time: a Cd document with no artist is rejected by Mongoose before MongoDB ever
sees it.
This chapter builds the same real shapes — the same field names, the same required fields, even the same
itemType field name — in Firestore. What Firestore can't reuse is the enforcement mechanism
itself: there is no Firestore equivalent of Item.discriminator(). A Firestore collection has no
declared shape at all, for any document, ever.
The Same Shapes, No Enforcement
Firestore's own flat document model actually makes reproducing the shapes easy — a document can hold any fields, so the base fields and a given item type's own fields just sit together directly on one document, with no separate base-versus-subtype schema objects needed at all:
Both documents land in the same items collection, side by side, exactly as the MongoDB sibling's
own discriminators arrange things — but nothing about that addDoc call checked that a
Cd document actually needs an artist, or that itemType is even one of
the four real values this app understands. A typo — itemType: "CD", capitalized differently from
every other document — would be accepted by Firestore exactly as readily as a correctly-typed one.
Field for field, this is the identical shape the MongoDB sibling settled on. The diagram above could be relabeled "MongoDB" and nothing in it would need to change — the real divergence between the two courses isn't in what gets stored, it's in what happens the instant before storage.
Closing the Validation Gap: a Real Shape Validator
Chapter 1 left this open rather than asserting an answer. Here's the answer, at least at the "what should be
checked" layer: a small, standalone function that knows this app's own real rules — which fields each item
type requires, and which fields have a restricted set of valid values, the direct equivalent of Mongoose's own
required: true and enum options:
Run against the two documents above, plus two deliberately broken ones:
That's the exact same class of rejection the MongoDB sibling's own discriminators give for free — a
Cd with no artist never reaches storage — reproduced by hand in plain JavaScript,
because Firestore itself will never reject it on its own.
validateItem() is deliberately written with no dependency on React, Express, or Firestore itself
— it just takes a plain object and throws or doesn't. That's on purpose: whether it ends up running in the
browser immediately before an addDoc call, inside a small backend route, or both, is exactly the
client-SDK-vs-backend-API question Chapter 1 raised and Chapter 3 resolves. Writing the check itself as a
standalone function now means that decision doesn't force this code to be rewritten later.
validateItem() before every addDoc() in this app's own code is a real
safeguard — but it's a safeguard that lives in application code, not in Firestore itself. Anyone with direct
write access to this Firestore project (via the console, a script, or a different app entirely) can still
write a malformed document straight past this function without ever calling it. Firestore Security Rules,
covered as part of Chapter 3's own architecture decision, are the one mechanism that can make a check like
this hold regardless of what code is doing the writing.
One Real Advantage Firestore Gets for Free
The MongoDB sibling's own Chapter 2 flagged a genuine limitation worth revisiting here: a document fetched
through the base Item model doesn't expose author, artist, or any other
discriminator-specific field, even though the field is really there in the underlying MongoDB document —
Mongoose only attaches those extra fields when a document is loaded through its own matching discriminator
model.
Firestore has no equivalent problem, for a simple reason: it has no separate base-model and subtype-model
objects at all. Every document fetched from the items collection comes back as a plain object
carrying every field it was actually written with:
Book.find() automatically filters to just Book documents because it's a
genuinely separate model tied to that discriminator. A Firestore query against items has no such
automatic filtering at all — finding just the Book documents needs an explicit
where("itemType", "==", "Book") clause every single time, covered properly once Chapter 6 builds
real search.
| Question | MongoDB sibling (Mongoose discriminators) | This course (Firestore) |
|---|---|---|
| Base fields | title, releaseYear, notes | Same three fields, same names |
| Type marker | itemType (the discriminatorKey) | itemType (a plain field, same name, deliberately) |
| Required-field enforcement | Built in, via required: true on each discriminator schema | Not built in — a real, hand-written validateItem() function |
| Restricted values (e.g. Bluray resolution) | Built in, via enum: [...] | Not built in — checked inside the same validator |
| Fetching a mixed list of every item type | One query, via the base Item model | One query, against the plain items collection |
| Do subtype fields show up on a generic fetch? | No — only via the matching discriminator model | Yes — every document is already complete |
| Fetching only one item type | Automatic — query through that discriminator model | Manual — an explicit where("itemType", "==", ...) |
Hands-On Exercises
Write real Firestore documents for all four item types (Book, Cd, Dvd, Bluray) into the same items collection using addDoc, then write a single getDocs query that returns all four back out, confirming every subtype field is present on each returned object with no extra step.
Extend validateItem() to also require a director field on Dvd and Bluray documents, then run it against one valid and one invalid document of each new type to confirm the new rule actually fires.
Explain, in your own words, why calling validateItem() from application code is a real safeguard but not the same guarantee Mongoose discriminators give — and name the one Firestore-native mechanism that could close that remaining gap.
Chapter 2 Quick Reference
- Same shapes as the MongoDB sibling —
title/releaseYear/notesplus per-type fields, one shareditemscollection, anitemTypefield distinguishing document shape - No built-in enforcement — Firestore accepts any document shape in any collection with no complaint
- The gap-closer — a standalone
validateItem()function reproducing Mongoose's ownrequired/enumchecks by hand - Still open — where
validateItem()actually gets called from (client-side before a write, or a backend route) is Chapter 3's own question - A free win — every fetched document already carries all of its own fields; no base-model/subtype-model split to trip over
- A real cost — filtering to one item type needs an explicit
where()clause every time, unlike Mongoose's automatic per-discriminator filtering - Next chapter: Firestore Reads & Writes From React — the Client SDK vs. a Backend API