---
title: "The shape of a Next.js app"
slug: the-shape-of-a-next-js-app
section: tech
date: 2026-05-29T09:17:00.000Z
canonical: https://roland.leth.ro/blog/tech/the-shape-of-a-next-js-app
---

The other month I rebuilt my site on Next.js. I want to write about it, but not the migration itself, which is mostly a long list of "this was the equivalent on the new side." What's worth writing about is the _shape_ of the app I ended up with, and the small handful of decisions that shaped everything that came after.

I'll start with what I did and _didn't_ pick.

## No framework on top of the framework

No NextAuth. No tRPC. No CMS. No Drizzle helpers layered on Prisma. The site is one user — me — writing posts to a database. Every dependency past that needs to earn its place.

Most "Next.js stack" posts read like a parts list. That's not how the choices felt to make. Most of mine were really one decision in different costumes: keep the surface area small, because a one-person site that I want to maintain for years is allergic to abstractions I'd have to relearn each time I open it.

So when I write "single user" in the rest of this series, it's load-bearing. A lot of choices below would be the wrong call on a team or for a multi-tenant app, and I'll flag where.

## Server components as the default

I went all-in on the App Router and server components, and the rule that kept the thing sane is: push client boundaries as far out toward the leaves as you can. A page is a server component unless it has to be otherwise. A section inside the page is a server component unless one of its children needs `useState`. The `"use client"` directive lives on the smallest piece that genuinely needs interactivity — a theme toggle, a carousel, a single input — and not one wrapper up.

That sounds like the obvious read of the React docs, but in practice the temptation is to mark the whole admin dashboard `"use client"` because three buttons inside it need state. Resist that, and Next.js gives you something pretty rare: a stack where most of your code never ships to the browser, and the small amount that does is so localized you can find every interactive piece by searching for the directive.

The downside is real, though: there are entire categories of bug you only hit because of the server-component model. I have a whole post planned on three of them. They're worth the price; they're not zero.

## Boring data, picky boundaries

Postgres on Vercel, Prisma as the ORM, Zod at every request boundary. Nothing exotic. The interesting work was at the edges, not at the database layer.

Two patterns kept the data layer honest:

- Every API route's request body goes through a small `parseJsonBody` helper that Zod-validates and only ever logs the failing field's path, never its value. I'll walk through it in the validation post.
- Every admin mutation writes a structured audit log entry with a fixed shape, derived from a `const [...] as const` array of tags. Adding a new admin resource is a single edit, and forgetting to log is a type error rather than a silent omission.

A lot of what's in the next few posts is more than this site needs. I built each defense because the cost of doing it right on a low-stakes project is small, and the cost of meeting it for the first time on a high-stakes one isn't. I'll flag where the line falls.

## Single-user auth, by hand

A small `proxy.ts` middleware checks a session JWT signed with `jose`; passwords are `bcryptjs`-hashed; the login route is per-IP rate-limited; everything's gated by a single `/api/admin/*` matcher. It's about 200 lines total. NextAuth would have done all of this, plus thirty other things I don't need. "200 lines I can read in one sitting" beat that trade for me.

The thing I had the most fun with in this layer is the rate-limit IP pseudonymization story: a small piece of privacy-aware engineering that turns out to be load-bearing in a way I didn't expect. That's [the next post][2].

## Tailwind only, dark mode day one

No CSS-in-JS, no styled-components, no module CSS. Just Tailwind, a small set of theme tokens, and the system color scheme. Dark + light from day one, because retrofitting either onto a site that's been live for a year is the kind of avoidable misery I've stepped on enough times.

This isn't a hot take any more, but it bears repeating: in 2026, "you'll regret Tailwind in two years" has had time to play out, and largely hasn't. The codebase doesn't have a styling layer that grows; it has class names on elements, which is where you were looking anyway when you opened the file.

## Vercel, with a small dependency on Upstash

Hosted on Vercel. The only piece of state that needs to outlive a request — the per-IP rate-limit bucket on the login route — lives in Upstash Redis. Everything else is the database. The free tier covers what a personal site sees comfortably, and the SDK reads like normal Redis rather than inventing its own vocabulary.

## What the rest of the series is

A handful of posts about interesting topics and decisions I made along the way:

1. The shape of a Next.js app _(this one)_
2. [Auth for a single-user site, without NextAuth][2]
3. [Input validation and logs that don't leak][3]
4. [The admin write contract][4]
5. [200 markdown posts and the future-dated content puzzle][5]
6. [Three surprises about server components][6]
7. [Three markdown pipelines and a theme toggle][7]
8. [What I'd do differently][8]
9. [The tools that keep me out of the admin][9]

Each one will lean on a bug or constraint that made me reach for the final solution. If any of the patterns above made you wince, the relevant post should explain itself. If it doesn't, I'd love to hear about it.

[2]: /blog/tech/auth-for-a-single-user-site-without-nextauth "Auth for a single-user site, without NextAuth"
[3]: /blog/tech/input-validation-and-logs-that-dont-leak "Input validation and logs that don't leak"
[4]: /blog/tech/the-admin-write-contract "The admin write contract"
[5]: /blog/tech/200-markdown-posts-and-the-future-dated-content-puzzle "200 markdown posts and the future-dated content puzzle"
[6]: /blog/tech/three-surprises-about-server-components "Three surprises about server components"
[7]: /blog/tech/three-markdown-pipelines-and-a-theme-toggle "Three markdown pipelines and a theme toggle"
[8]: /blog/tech/what-id-do-differently "What I'd do differently"
[9]: /blog/tech/the-tools-that-keep-me-out-of-the-admin "The tools that keep me out of the admin"
