How a Web App Loads Data Safely
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:
The implementation in *.server.ts touches the database. Dynamic import() keeps DB code out of the client bundle.
Step 2 — Define one query config
Step 3 — Prefetch in the route loader
Step 4 — Read the cache in the component
Same config object. Same cache. No duplicate fetch on hydration.
Step 5 — Handle writes with mutations
Forms use useMutation pointing at another server function:
Common pitfalls
- Importing
dbdirectly in a React component — secrets leak to the bundle. - Different query keys in loader vs component — double fetch and hydration mismatch.
- Fetching in
useEffecton 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.