Skip to main content
Engineering

How a Web App Loads Data Safely

10 min read

Your database password must never ship to the browser. If DATABASE_URL appears in client JavaScript, anyone can extract it from the bundle.

The fix: a server boundary. The browser calls a server function; the server talks to Prisma; the result comes back as HTML (SSR) or JSON.

Vocabulary

Term Meaning
Server function A typed RPC call that only runs on the server
Query config One object pairing a cache key with a fetch function
Loader Route hook that prefetches data before HTML is sent
Hydration React attaching to server-rendered HTML in the browser
Mutation A write operation (form submit, delete) via useMutation

Steps

Step 1 — Write the server function

The wrapper lives in a *.functions.ts file — safe to import from client code:

typescript

The implementation in *.server.ts touches the database. Dynamic import() keeps DB code out of the client bundle.

Step 2 — Define one query config

typescript

Step 3 — Prefetch in the route loader

typescript

Step 4 — Read the cache in the component

typescript

Same config object. Same cache. No duplicate fetch on hydration.

Step 5 — Handle writes with mutations

Forms use useMutation pointing at another server function:

typescript

Common pitfalls

  • Importing db directly in a React component — secrets leak to the bundle.
  • Different query keys in loader vs component — double fetch and hydration mismatch.
  • Fetching in useEffect on SSR pages — empty first paint, bad SEO.

Verify it works

View page source — content should appear in HTML before JavaScript runs. Navigate away and back — cached data should load instantly.

Takeaway

One query config, two call sites (loader + component). Server functions for reads and writes. TanStack Query bridges SSR and client navigation.