Farewell, Next.js: Server-Rendered React in Five Dependencies

Part of Own your stack: a smaller foundation for your product

#react#server-components#developer-tools+1 more

Illustration — Farewell, Next.js: Server-Rendered React in Five Dependencies

For years the answer was Next.js, and it earned it. But server-rendered React still came with a trade-off: fast first paint or app-like speed, rarely both. That has changed. You can now have instant load and smooth navigation in a stack small enough to actually own.

React 19 turned server rendering into a primitive. Server Components, server functions and streaming used to be things a framework did for you. Now React does them, and you can hold them directly. So we asked a simple question and answered it with running code instead of opinions: how small and how yours can a server-rendered React stack be?

We built it. It's own-stack, running live here, and everything below is something the repo actually does.

The whole thing, in numbers

Five production dependencies. When we published, the complete demo app was fifteen files and about four hundred lines: home page, a server-rendered feed, and a guestbook with a real mutation.

That's the entire stack:

"dependencies": {
  "hono": "...",
  "react": "...",
  "react-dom": "...",
  "react-server-dom-webpack": "...",
  "waku": "..."
}

Waku is the only framework here: a deliberately minimal React Server Components meta-framework from the author of Jotai and Zustand. It gives you SSG, SSR and file-based routing, and then gets out of the way. Everything else is just React.

How a request actually flows

Loading diagram…

Mermaid source
flowchart LR
    REQ(["Request"]) --> WAKU["Waku · SSG / SSR / routing"]
    WAKU --> RSC["Server Components\n(run on the server)"]
    RSC -->|await, fully typed| DATA["Server functions\n(data + mutations)"]
    RSC -->|server-rendered HTML| OUT(["Page"])
    RSC -.->|only the interactive bits| ISLE["'use client' islands"]
    ISLE --> OUT

The shift worth internalising: the component is the server now. There's no client that fetches from an API that talks to a database. The page runs where the data is.

Types across the wire, for free

The piece people reach for tRPC to solve, sharing types between server and client, mostly evaporates. A Server Component imports a server function and awaits it:

// the data layer is a plain typed module
export async function getFeed(): Promise<FeedItem[]> { /* db, api, anything */ }

// the page awaits it directly — `items` is FeedItem[], no glue
const items = await getFeed();

There is no API route, no schema, no codegen, no client. The import is the contract. Mutations work the same way through a server action: a "use server" function the client calls like a local async function, with the signature enforced on both ends. The guestbook in the repo does exactly this, a typed write across the network with no API layer in sight.

You feel the difference at once: the boilerplate is gone. No app/api/feed/route.ts, no fetch wrapper, no query hook, no Zod schema duplicated on both sides, no generated client to keep in sync. You write a function and you call it. Interactivity is opt-in the same way. A page is server-rendered by default, and you reach for a "use client" island only where something needs to be interactive.

INFO

The mental model is subtraction. No API layer (Server Components fetch directly). No tRPC (the import is the type). No build-time codegen (types flow through imports). No Tailwind (one semantic stylesheet). You add interactivity deliberately, instead of removing weight you never chose to carry.

On styling we kept our house rule: pure semantic CSS, no Tailwind, no CSS-in-JS. The Waku starter ships with Tailwind; removing it was three files and a cleaner stylesheet. Style the primitives, let the cascade work.

The part that scales: the server owns the state

There's a less visible consequence, and it's the one that matters in production. In the classic React app, state lives in two places at once: on the server, which holds the truth, and in a client cache that has to be kept in agreement with it (TanStack Query, SWR, a Redux store, pick your instrument). That synchronisation layer is where a large share of the bugs live, and it's an entire tier you design, ship and operate.

In this model the server is the store. A Server Component reads data where the data already lives and renders it; a server action mutates it and the page re-renders from that same source of truth. There's no second copy to drift and no cache to invalidate, because the state was only ever kept in one place.

That shrinks the infrastructure as much as the code. No client cache means no separate API tier whose whole job is to feed that cache: fewer services to deploy and fewer failure modes strung between the database and the pixel. The stack gets smaller exactly where production systems usually grow another tier.

The honest frontier: auth

This is where owning your stack runs into a young ecosystem. I won't pretty it up.

We wanted Better Auth, a framework-agnostic, TypeScript-native auth library that lives inside your app instead of behind a vendor. It's exactly the right shape for a stack like this. But when we tried to mount an arbitrary request handler in Waku through the obvious file convention, we got a clean 404, and concluded it needed Waku's programmatic createApi. In Next, auth is a documented drop-in; here, we were wiring the plumbing ourselves.

That's the real trade. Owning your stack means meeting the edges of a framework that's young precisely because it's minimal. The RSC core is mature and a pleasure to use. The API ergonomics are still moving fast.

INFO

Update, 30 September 2026. The mount was never missing. Waku serves raw request handlers from src/pages/_api/; we had probed the wrong folder and blamed the framework. own-stack now signs people in with passkeys, checked with WebCrypto and no auth library, still on five production dependencies: try it.

What we'd ask you to take from this

The default isn't a law. "Server-rendered React" stopped meaning "a single framework that owns the whole thing" the moment React 19 shipped Server Components as a primitive. You can now assemble a stack that is small, typed end to end, scales by removing tiers rather than adding them, and is yours. Where it's still young, the edges are in plain sight and you can test them yourself.

We're not telling you to rip out Next tomorrow; for many teams its maturity is the right call today. We're saying the assumption deserves re-examination, and the building blocks are finally good enough to examine it honestly. The starter is open and runnable: try the live demo, or clone it, delete what you don't need, and see how little is actually left.

Part two carries the same idea across the full stack, with typed client calls and no tRPC or TanStack.

Let’s talk

What could we build together?

Your note goes to the Hyperdrift team, who also build Orchestra. We’ll use your email to reply about this enquiry.