Deployment

Food Tracker (React + Express + Prisma)

Chapter 11 · Deployment

Every prior chapter ran two separate processes — Vite's dev server and Express — talking across the CORS boundary Chapter 1 set up. A real deployment collapses that back down to one. Most of that story is identical to the sibling course; Prisma adds two real, extra steps to the pipeline that better-sqlite3 never needed.

Building the Client

cd client npm run build # produces client/dist/ — a static index.html, JS, and CSS bundle

Serving the Build From Express

// server/index.js import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // API routes registered first app.use("/api/items", itemsRouter); app.use("/api/lookup", lookupRouter); app.use("/api/recipes", recipesRouter); // static build + SPA fallback registered last if (process.env.NODE_ENV === "production") { const clientDist = path.join(__dirname, "../client/dist"); app.use(express.static(clientDist)); app.get("*", (req, res) => { res.sendFile(path.join(clientDist, "index.html")); }); }
Registration order is not cosmetic — unchanged from the sibling course
Express matches routes in the order they're registered, and app.get("*", ...) matches every path, including /api/items. If the catch-all were registered before the API routers, every single API request would be swallowed by it, silently returning index.html instead of JSON. The API routes must always be registered first — nothing about Prisma changes this; it's an Express-level fact.

React Router handles navigation entirely in the browser — a URL like /history never actually exists as a real file on the server. app.get("*", ...) always returns index.html instead of a 404, letting React Router take over and render the correct view client-side once the page loads.

The honest cost of "no backend you write" not applying here — unchanged from the sibling
Food Tracker (React + Firebase)'s own deployment chapter got an SPA rewrite rule and automatic HTTPS from Firebase Hosting configuration alone. This course has to hand-write both, exactly like its raw-SQL sibling — the catch-all route above is this course's own SPA rewrite rule, and HTTPS termination has to be arranged separately too.

Prisma's Own Real Deployment Steps

Neither of these exists for the sibling course, and skipping either one produces a real, working-locally-but-broken-on-the-server failure:

# on the deploy server, after pulling the latest code npm install npx prisma generate # regenerate Prisma Client for this environment npx prisma migrate deploy # apply any migrations not yet run on this database npm run build # builds the React client, as above
Why prisma generate runs again here, even though schema.prisma hasn't changed
Prisma Client is generated code, written into node_modules — and node_modules is never committed to version control. A fresh npm install on the deploy server rebuilds node_modules from scratch, which means the generated client has to be rebuilt there too. The @prisma/client package does include a postinstall hook that usually runs prisma generate automatically after npm install — but environments that skip install scripts (a Docker multi-stage build, certain CI caching setups run with --ignore-scripts) can silently skip it. Running prisma generate explicitly, rather than trusting the hook alone, is the defensive, honest version of this step.
migrate deploy is not migrate dev — and the two must not be confused
prisma migrate dev, used throughout this course's own local development, can create new migrations and, in some drift scenarios, prompt to reset the development database — genuinely useful behavior while building, genuinely dangerous in production. prisma migrate deploy only applies migrations that already exist as committed files in prisma/migrations/; it never generates a new one, and it never prompts. It's the one Prisma command actually meant to run against a real, populated database.
Deployment stepSibling courseThis course
Build the clientnpm run buildSame
Prepare the backendNothing extra — better-sqlite3 needs no separate stepnpx prisma generate
Apply the schemaAlready applied — db.exec(schema.sql) runs at server startupnpx prisma migrate deploy
Start the processpm2 start server/index.jsSame

Environment Configuration and Process Management

# .env (production) NODE_ENV=production PORT=3001 DATABASE_URL="file:./prod.db"

A plain node server/index.js process exits the moment it crashes, and doesn't restart on its own. A process manager like pm2 keeps the server running, restarting it automatically on a crash or a server reboot:

npm install -g pm2 pm2 start server/index.js --name foodtracker pm2 startup # configures pm2 to launch on system boot

TLS Termination

Chapter 4's own getUserMedia requires HTTPS in production — no exception. Express itself doesn't handle TLS certificates; the standard approach is a reverse proxy (nginx) sitting in front of the Node process, terminating HTTPS and forwarding plain HTTP internally. This has nothing to do with Prisma either way.

The sibling's own persistent-volume gotcha, now expressed through DATABASE_URL
Because the entire database is one file on disk, deploying to a platform with an ephemeral filesystem — one that wipes local storage on every redeploy or restart — would silently lose every item ever tracked, exactly as in the sibling course. The difference here is just where that file path lives: not a hardcoded string inside db.js, but the DATABASE_URL value in .env. Confirming the deployment target keeps whatever path DATABASE_URL points at across restarts is the same real, easy-to-overlook step either course needs — it just needs checking in a different file here.

Where This Course Is Headed

One chapter left: a capstone tying every chapter into one complete, working app.

Hands-On Exercises

Exercise 1

Explain exactly what would happen to a request for /api/items if the catch-all route were registered before the API routers instead of after, and why no error would appear anywhere to signal the problem.

📄 View solution
Exercise 2

Explain the real difference between prisma migrate dev and prisma migrate deploy, and why only the second one belongs in a deployment script.

📄 View solution
Exercise 3

Explain why this chapter runs npx prisma generate explicitly on the deploy server rather than relying solely on @prisma/client's own postinstall hook.

📄 View solution

Chapter 11 Quick Reference

  • Build: npm run build produces client/dist/, served via express.static — identical to the sibling
  • Order matters: API routes must be registered before the catch-all — an Express fact, unrelated to Prisma
  • Two real extra steps: npx prisma generate (rebuild the generated client on this environment) and npx prisma migrate deploy (apply committed migrations, production-safe)
  • Don't confuse the two migrate commands: migrate dev can create migrations and reset the dev database; migrate deploy only applies what's already committed, never prompts
  • Process management & TLS: pm2 and an nginx reverse proxy, identical to the sibling
  • Real gotcha, relocated: the SQLite file still needs a persistent volume — now tracked via DATABASE_URL in .env rather than a hardcoded path
  • Next chapter: Capstone