Firestore Reads & Writes From React: The Client SDK vs. a Backend API
Personal Catalogue: React & Firebase
Chapter 3 · Firestore Reads & Writes From React: The Client SDK vs. a Backend API
Two chapters have now written directly to Firestore from plain scripts and left one real question hanging both times: does this app ever get a backend server of its own, or does React talk to Firestore directly for good? This chapter answers it, for real, and builds the one piece that answer actually depends on.
A Confession About the Last Two Chapters' Own Code
Chapter 1 said to enable Firestore in production mode, specifically because test mode leaves
every document wide open. What that also means, honestly: if the addDoc/getDocs code
from Chapters 1 and 2 was actually run against a real production-mode project, every single call would have
failed. Firestore's own real production-mode default is a deny-all rule:
Nothing about that is a mistake in either earlier chapter — the code shown was always correct, and this chapter is what actually turns it on.
Resolving the Fork: No Backend Server of This Course's Own
Chapter 1's own finding held up: nothing in this app's real v1 scope needs a secret, and nothing needs a Cloud Function to hold one. Given that, introducing a small Express-style API layer here would buy back exactly nothing this variant doesn't already get from the client SDK alone — it would just reintroduce the self-run server this course exists specifically to do without. So the real decision: React talks to Firestore directly, using the client SDK, for every read and write this app makes. There is no backend process of this course's own at all.
That decision puts real weight somewhere else, though — Chapter 2's own warn-box already named it. With no
server standing between the browser and the database, validateItem() alone was never going to be
enough on its own; it's a function, not a gate anyone is forced to pass through. Something has to actually
enforce it. That something is Firestore Security Rules.
Turning Chapter 2's Checks Into Real Rules
Security Rules are evaluated by Firebase's own servers on every single request, regardless of what code sent
it — a script, a browser tab, or a stray line typed straight into the Firebase console. Translated directly
from Chapter 2's own validateItem() logic:
Deployed with firebase deploy --only firestore:rules, this is the exact same rule set the
MongoDB sibling's own discriminator schemas enforce, expressed as declarative rules instead of JavaScript
classes — and, critically, applied by Firestore itself rather than by any one function someone has to
remember to call.
Proving It — Including on Purpose Skipping validateItem()
A valid write, with validateItem() called first exactly as Chapter 2 intended:
Now the deliberate skip — calling addDoc directly, exactly the "a different code path forgot to
validate" scenario Chapter 2's own warn-box raised:
validateItem() — it never ran. Firestore itself refused the
write because hasTypeRequiredFields() evaluated to false against the real document being sent.
This is the universal guarantee Mongoose discriminators give the MongoDB sibling for free, now genuinely
present here too — just enforced by rules instead of by a schema class.
What Security Rules Still Can't Do Here
allow read: if true above is a deliberate, honest choice, not an oversight. This app has no
Firebase Authentication, no user accounts, and no login of any kind — a decision this course made back in
Chapter 1, in keeping with a genuinely personal, single-user tool. Firestore's own request.auth
variable, the normal way a rule checks who is making a request, is always null here,
for every request, because no one has ever signed in through this app at all.
request.auth.uid,
the same mechanism the site's own Food Tracker (React + Firebase) course reaches for once it adds
Authentication, would become necessary here too.
The Architecture, Now Fully Resolved
| Layer | MongoDB sibling | This course |
|---|---|---|
| Where a request lands first | An Express route, running on a server this project starts and keeps running | Firestore itself — React calls the client SDK directly, no server of this course's own |
| Shape validation | Mongoose discriminator schemas, checked in the Express process | Firestore Security Rules, checked by Firebase's own servers |
| Instant client-side feedback | Whatever the frontend chooses to check before sending the request | validateItem(), called before every write — a UX convenience, not the real gate |
| The real, universal gate | The Express route + Mongoose, together | Security Rules alone |
| Who can write | Whoever can reach the Express server (no auth built for this project either) | Anyone who can reach the Firestore project — no request.auth check exists to make |
Every remaining chapter in this course builds directly against this: React components calling
validateItem() then a Firestore SDK function, with the Security Rules deployed here standing
permanently behind them.
Hands-On Exercises
Deploy the Security Rules above to a real Firestore project (or the local emulator), then call addDoc directly — bypassing validateItem() entirely — with a Book document that has no author. Confirm the write is rejected and record the real error message.
Explain why allow read: if true is a reasonable choice for this specific app today, and name the exact condition under which it would stop being reasonable.
Explain what real value validateItem() still adds once Security Rules are deployed, given that the rules alone are now the actual, universal gate — in other words, why wasn't Chapter 2's own work wasted effort now that Chapter 3 exists?
Chapter 3 Quick Reference
- Resolved — no backend server of this course's own; React talks to Firestore directly via the client SDK
- Why — nothing in v1 scope needs a secret (Chapter 1's own finding), so a server would add cost with no real benefit
- The real gate — Firestore Security Rules, translated directly from Chapter 2's own
validateItem()logic, enforced by Firebase itself regardless of what code is writing - Production mode's real default — deny-all until rules like these are deployed; earlier chapters' own code only starts working now
validateItem()'s real remaining job — instant, round-trip-free feedback in the UI; a convenience layered on top of the real gate, not a replacement for it- Honest limit — rules here check document shape only, never who's writing, since this app has no Authentication layer at all
- Next chapter: The Add-Item Form — type-specific fields and client-side validation