Capstone

Personal Catalogue: React, Express & MongoDB

Chapter 10 · Capstone

Chapter 9 got the whole stack running as a real, standalone deployment. This closing chapter does what Chapter 1 always meant it to do: put it somewhere the user actually visits — a real path under their own existing Astro-based site — and lay out a genuine, concrete plan for the one feature deliberately deferred since Chapter 1, barcode scanning.

Three Real Ways to Mount This Behind an Existing Site

OptionWhat it involvesTradeoff
Subdomain (catalogue.existing-site.com)A separate DNS record and its own nginx server block pointing straight at the unified Express app from Chapter 9Simplest possible nginx config; feels like a separate site rather than one integrated experience
Path-mounted reverse proxy (existing-site.com/catalogue/)One nginx location block on the existing site's own domain, proxying that one path to the same Express appFeels like part of the same site; needs two real, verified fixes this chapter builds — covered below
JSON-API + Astro-frontend rewriteKeep only Chapters 1-6's backend; rebuild the UI as new Astro pages/components consuming it directlyThe deepest real integration, and the most rework — appropriate only if Astro should become the one true frontend long-term

The path-mounted option is the one built out in full here — genuinely integrated, without discarding any of the nine chapters of real React frontend already built.

A Real Problem: The Built Assets Don't Know They're Not at the Root

Chapter 9's own npm run build produces an index.html referencing its own JS and CSS bundles with paths like /assets/index-abc123.js — absolute from whatever domain root the app is served under. That's correct for Chapter 9's own standalone deployment, and genuinely wrong the moment this app is mounted at /catalogue/ on someone else's domain: a request for /assets/index-abc123.js resolves against the domain's own root, not the real location the file actually lives at, /catalogue/assets/index-abc123.js.

Verified Against Vite's Own Documentation
Vite's base config option exists specifically for this. Left unset, every generated asset reference assumes a root deployment; setting it rewrites every one of those references — "JS-imported asset URLs, CSS url() references, and asset references in your .html files," per Vite's own documentation — to include the real sub-path prefix.
// catalogue-web/vite.config.js export default defineConfig({ plugins: [react()], base: '/catalogue/', });

Rebuilding with this in place produces an index.html referencing /catalogue/assets/index-abc123.js instead — correct for exactly where the file is actually going to be requested from once this specific deployment goes live.

A Second Real Problem: API Calls Are Root-Relative, Not Site-Relative

Chapter 9's own API_BASE_URL falls back to an empty string in production, so every fetch call becomes a path like /api/items — correct for a standalone deployment at the domain root, but still wrong here for a genuinely different reason than the assets above: /api/items resolves against the domain's own root regardless of which page made the request, so a request made from existing-site.com/catalogue/ would still be sent to existing-site.com/api/items — not existing-site.com/catalogue/api/items, where this app's own API actually needs to be reached once mounted at a sub-path.

The fix reuses Chapter 9's own mechanism, this time with a real value instead of an empty fallback:

# catalogue-web/.env.production — new for this specific deployment VITE_API_URL=/catalogue

Every fetch call built since Chapter 4 already has the shape ${API_BASE_URL}/api/items — with API_BASE_URL now resolving to /catalogue, that becomes /catalogue/api/items, matching exactly where the reverse proxy below is about to make this app's own API actually reachable.

Two Genuinely Different Fixes for Two Genuinely Different Things
base is a build-time setting controlling where the compiled JavaScript and CSS files are referenced from. VITE_API_URL is a runtime value controlling where this app's own fetch calls are sent. Both happen to need the identical /catalogue prefix for this specific deployment, but fixing one without the other leaves either a broken page (missing assets) or a broken app (failing API calls) — not both problems solved by a single change.

Building Out the Reverse Proxy

One location block on the existing site's own nginx config, added alongside whatever configuration already serves the rest of that site:

# /etc/nginx/sites-available/existing-site (excerpt) server { listen 443 ssl; server_name existing-site.com; # ...the existing Astro site's own location blocks, unchanged... location /catalogue/ { proxy_pass http://localhost:4000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

The trailing slash on both location /catalogue/ and proxy_pass http://localhost:4000/; is what makes nginx strip the /catalogue prefix before forwarding — a request for /catalogue/api/items reaches Express as a request for exactly /api/items, the same route Chapter 3 already defined with no changes needed on the Express side at all. The same stripping applies to a request for /catalogue/assets/index-abc123.js, which reaches Express as /assets/index-abc123.js — exactly what Chapter 9's own express.static() is already serving.

No New JSON Endpoint Needed Here
The PHP and Django siblings' own capstones each had to add a small, new JSON API endpoint as a first step toward deeper integration, since their own apps were server-rendered pages without one. This course never needed that step — every route has returned real JSON since Chapter 3, and the React frontend has consumed it as an API from Chapter 4 onward. There's nothing left to add on that front; only the two path-prefix fixes above were actually missing.

With everything mounted this way, the browser only ever sees one domain — Chapter 9's own ALLOWED_ORIGIN CORS setting can simply be set to https://existing-site.com, and every request genuinely is same-origin now, exactly the scenario that chapter's own warn-box anticipated without yet knowing which real shape it would take.

A Real, Concrete Plan for Barcode Scanning

Chapter 1 deferred barcode scanning explicitly rather than pretending it wasn't wanted. With the app actually live, the real plan for adding it later:

  • Camera access — a real, actively maintained library like html5-qrcode handles reading a barcode from the device camera directly in the browser, without waiting on the native BarcodeDetector API's own still-inconsistent cross-browser support.
  • Book lookup — a scanned ISBN can be sent to Open Library's real, free, no-API-key-required endpoint, https://openlibrary.org/isbn/<isbn>.json, to auto-fill title, author, and publisher — reducing Chapter 8's own fast manual entry down to "scan, confirm, save" for the one item type with a clean, verified free lookup source.
  • Cd/Dvd/Bluray lookup — honestly still an open question. Unlike ISBNs for books, there's no single, universally reliable free UPC-lookup service this course has actually verified for physical media — real research into a genuinely dependable option is the concrete next step here, not something to guess at.
  • Where the lookup call should live — a new Express route (something like GET /api/lookup/isbn/:isbn), matching this project's own established pattern of keeping external API calls server-side rather than scattering direct third-party requests across React components, the same architectural choice Chapters 3 and 6 already made for this app's own data.

Chapter Attribution

Piece of the finished appBuilt in
Heterogeneous item schema via Mongoose discriminatorsChapter 2
CRUD API with dynamic dispatch to the right modelChapter 3
Add-item form, driven by FIELD_CONFIGChapter 4
Tag editing via atomic $addToSet/$pullChapter 5
Cross-type search, escaped and correctly orderedChapter 6
List and detail views, reusing FIELD_CONFIG for displayChapter 7
Friction reduction: inline errors, focus, live duplicate checkChapter 8
Unified single-process deployment, API_BASE_URL, PM2, least-privilege MongoDB userChapter 9
Path-mounted reverse proxy, Vite base, sub-path API_BASE_URL, barcode-scanning planChapter 10 (this chapter)

Hands-On Exercises

Exercise 1

Set base: '/catalogue/' in vite.config.js and VITE_API_URL=/catalogue in .env.production, rebuild the frontend, and confirm the built index.html's own asset references and every compiled fetch call both correctly include the /catalogue prefix.

📄 View solution
Exercise 2

Configure the nginx location /catalogue/ block against a real running instance of the app (a second local nginx config is fine for testing), and confirm both a page load and a real API request succeed through the proxy, with Express's own server.js completely unchanged from Chapter 9.

📄 View solution
Exercise 3

Set only base in vite.config.js without also setting VITE_API_URL (leaving Chapter 9's own empty-string fallback in place), rebuild, and confirm the page itself loads correctly while every API call still fails — demonstrating why the chapter's own warn-box treats these as two separate fixes, not one.

📄 View solution

Chapter 10 Quick Reference — Course Complete

  • Three mounting options — subdomain, path-mounted reverse proxy (built here), or a full Astro-frontend rewrite
  • vite.config.js's base — a build-time fix for asset references, verified against Vite's own documentation
  • VITE_API_URL=/catalogue — a runtime fix for fetch calls, reusing Chapter 9's own API_BASE_URL mechanism with a real value instead of an empty fallback
  • nginx's trailing-slash prefix-stripping — lets Express's own routes stay completely unchanged from Chapter 9
  • No new JSON endpoint needed — unlike the PHP/Django siblings, this app has been API-first since Chapter 3
  • Barcode scanning, planned honestly — a real library and a real, verified free ISBN lookup for books; UPC lookup for the other three types named as real, unresolved future research
  • This closes the course — 10/10 chapters, matching the shared four-course Personal Catalogue spec from Chapter 1