webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
Data and backend · Lesson 4 of 4

Postgres in Next.js

The database only on the server, Neon and pooling, caching by kind of data, seeding and migrations.

Updated

What connecting Next.js to a database means

In Next.js, the database is accessed only on the server: in Server Components, Server Actions and Route Handlers. A component in the browser never talks to the database directly — it always goes through your server code, which checks who's asking and for what.

This app is exactly that kind of project: Next.js 16 + Postgres on Neon.

"Serverless" Postgres — why Neon, Supabase, Vercel Postgres

A classic database keeps connections open permanently. Serverless functions (Vercel) start and stop often — each one would open new connections and exhaust the limit.

Solution How it solves it
an HTTP driver (@neondatabase/serverless) each query is an HTTP request — no connection to keep
connection pooling (PgBouncer, a URL with -pooler) a middleman reuses a few real connections
scale to zero the database stops when it isn't used (a low cost for personal projects)
branching (Neon) an instant copy of the database for every preview / PR

The structure in the project

  • .env.localDATABASE_URL — never in Git
  • src/
    • shared/
      • api/
        • db.tsthe client, with import 'server-only'
    • entities/
      • lesson/
        • api/
          • lesson-api.tsqueries with 'use cache' + cacheTag
      • progress/
        • api/
          • progress-api.tsper-user reads (dynamic)
          • save-progress.tsthe write Server Action
  • db/
    • schema.sqlthe table structure
  • scripts/
    • seed.tsinitial data
// shared/api/db.ts
import 'server-only'
import { neon } from '@neondatabase/serverless'

export const sql = neon(process.env.DATABASE_URL!)

server-only makes the build fail if the module ever ends up in a Client Component.

Reading, writing, caching — the full pattern

Operation Where Cache
public data (lessons, products) a function in entities/*/api 'use cache' + cacheLife + cacheTag
per-user data (progress, a cart) a function that reads cookies() / the session dynamic, inside <Suspense>
writing a Server Action: auth → Zod validation → query → updateTag / refresh() invalidation

All three are implemented in this app — see Caching and Server Actions.

Seeding and test data

A seed fills the database with initial data (the lesson content here comes from npm run seed). The rules: a seed is idempotent (running it twice gives the same result — INSERT ... ON CONFLICT DO UPDATE), and it isn't run on production with test data.

A production checklist

  • DATABASE_URL set in Vercel separately for Production and Preview (ideally, a separate Neon branch for previews).
  • The pooler URL for serverless functions.
  • Migrations run before deploying the code that depends on them.
  • Indexes on the columns used in WHERE and JOIN.
  • Backups / point-in-time restore enabled.

Summary

  • The database only on the server; the DB client with server-only.
  • Serverless → an HTTP driver or pooling; Neon also offers branching for previews.
  • Public → 'use cache'; per user → dynamic inside Suspense; writes → a Server Action + invalidation.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.