Blog
Software2024-06-0513 min

Next.js App Router + Prisma patterns we use across all our projects

After building Verixo, OmniTrip, AAETAV and Tuki with the same stack, repeatable patterns emerged that accelerate development and avoid the most common bugs.

The stack we converged on after four projects

After building four distinct products with Next.js 14 App Router, Prisma and PostgreSQL, we consolidated a set of patterns we use consistently. These are not dogmas, they are solutions to problems that bit us more than once.

Pattern 1: Prisma client singleton with configurable logs

The most common error with Prisma in Next.js is instantiating multiple clients in development, which exhausts database connections. The solution is a singleton that uses globalThis to persist the instance between Next.js hot-reloads.

Pattern 2: Server Actions with two-layer Zod validation

Server Actions are real HTTP endpoints: anyone can call them directly without going through the client. Our pattern: Zod validation on both client (for fast UX) and server (for real security). The Zod schema is defined once and imported in both places.

Pattern 3: Protected routes with Next.js middleware

The middleware reads the JWT from the cookie, verifies it, and if invalid redirects to login. If valid, it adds headers with user data that Server Components read without touching the database. This centralizes authentication logic in one place.

Pattern 4: Server Components with Suspense for slow data

Instead of showing a global spinner while the entire page loads, we use React Suspense to show the page partially while slow data loads in the background. Next.js streams the HTML, sending static parts first.

Pattern 5: Cache invalidation with revalidateTag

We assign tags to database queries and invalidate them in Server Actions that mutate that data. Example in OmniTrip: when a booking is created, we invalidate the departures-availability tag that affects the public calendar.

What we don't use

tRPC: with Server Actions the configuration overhead is not worth it, they are already natively type-safe in Next.js. Zustand or Redux for global state: with Server Components, most global state lives in the URL or in the database. For remaining UI state, local useState is sufficient.