De-mystifying the meta-framework. A "build-your-own" implementation of Next.js core features from scratch.
Un-nexted is a raw implementation of the server-side rendering (SSR) pipeline that powers modern web frameworks. It strips away the complexity of production codebases to reveal the fundamental architecture: how a server turns React components into HTML strings, and how the browser "hydrates" that static HTML into an interactive app.
I used Next.js daily but treated it as a black box. I knew how to use getServerSideProps and file-based routing, but I didn't truly understand how they worked.
I built Un-nexted to answer specific engineering questions:
- Routing: How does a file on a disk (
pages/about.tsx) become a URL route (/about) without me writing a router config? - SSR vs. CSR: How exactly does the server send HTML that React can "pick up" later?
- Hydration: What actually happens during
hydrateRoot, and why do hydration mismatch errors occur? - Bundling: How do we bundle code differently for the server (Node/Bun) vs. the browser?
Key Insight: Next.js is "just" React + a Compiler + a Server. The "magic" is mostly string manipulation, glob patterns, and careful state synchronization.
The project implements the "Isomorphic" React flow in four distinct stages:
graph TD
subgraph Build["1. Build Step"]
Bundler[Bun.build] -->|Compiles| ClientJS[Client Bundle]
end
subgraph Server["2. Server (SSR)"]
Req[Request] --> Router{Router}
Router -->|Match| Page[Page Component]
Page -->|Fetch Data| GSSP[getServerSideProps]
Page & GSSP -->|Render| HTML[HTML String]
end
subgraph Transport["3. Transport"]
HTML -->|Inject Data| Window[window.__DATA__]
end
subgraph Client["4. Client (Hydration)"]
Browser[Browser] -->|Load| ClientJS
ClientJS -->|Hydrate| Window
end
ClientJS -.-> Browser
HTML -.-> Browser
- The Build Step (Bundler):
- Uses
Bun.buildto compile client-side code. - Separates server-only logic (secrets, DB calls) from client bundles.
- Uses
- The Server (SSR):
- Interlopes requests and matches URLs to file paths.
- Executes
getServerSidePropsto fetch data. - Renders the component tree to a string using
react-dom/server.
- The Transport:
- Injects initial data (
__UNNEXTED_DATA__) into the HTML window object so the client doesn't need to refetch data.
- Injects initial data (
- The Client (Hydration):
- The browser loads the JS bundle.
- React reads the server-rendered HTML and attaches event listeners (Hydration).
# 1. Install dependencies
bun install
# 2. Start the development server (Builds + Serves)
bun run devVisit http://localhost:3000.
| Route | Feature Demonstrated |
|---|---|
/ |
Hydration: Static HTML becomes interactive (Counter). |
/users |
SSR Data: Fetches data on the server before rendering. |
/blog/hello-world |
Dynamic Routing: Matches [slug].tsx patterns. |
Every file has a specific purpose in the pipeline:
src/
├── app/
│ ├── server.ts # The HTTP server (matches URL -> Page)
│ ├── router.ts # The "Magic": Scans filesystem to build route map
│ ├── build.ts # The Bundler: Compiles client.tsx for the browser
│ └── client.tsx # The Entry Point: Runs in browser to hydrate DOM
├── pages/ # Your application code (Next.js style)
│ ├── index.tsx
│ └── blog/
│ └── [slug].tsx # Dynamic route example
└── types.ts
Instead of a static route config, we scan the directory at startup.
// router.ts logic
const glob = new Glob("src/pages/**/*.tsx");
for await (const file of glob.scan()) {
const route = file.replace("src/pages", "").replace(".tsx", "");
routes.set(route, file); // Maps "/about" -> "src/pages/about.tsx"
}We emulate Next.js's data fetching pattern by calling a static function on the component before rendering.
sequenceDiagram
participant Client as Browser
participant Server as HTTP Server
participant Router as File Router
participant Page as Page Component
Client->>Server: GET /url
Server->>Router: Match URL to File
Router-->>Server: Return "src/pages/..."
Server->>Page: Import Component
alt Has getServerSideProps
Server->>Page: Call getServerSideProps(ctx)
Page-->>Server: Return { props: ... }
end
Server->>Page: Render <Page {...props} /> (SSR)
Page-->>Server: Return HTML String
Server-->>Client: Send HTML + Initial State
Note over Client: React Hydrates DOM
// server.ts logic
const Page = await import(filePath);
let props = {};
// If the page needs data, fetch it on the server
if (Page.getServerSideProps) {
props = await Page.getServerSideProps(context);
}
// Render with data
const html = renderToString(<Page {...props} />);Create a file named with brackets, e.g., src/pages/blog/[slug].tsx.
// src/pages/blog/[slug].tsx
export default function Post({ params }) {
return <h1>Reading: {params.slug}</h1>;
}Export getServerSideProps to fetch data server-side.
// src/pages/users.tsx
export async function getServerSideProps() {
const data = await db.getUsers();
return { props: { users: data } };
}
export default function Users({ users }) {
return <div>{users.map(u => <div key={u.id}>{u.name}</div>)}</div>;
}It lacks production features like:
- Caching / ISR (Incremental Static Regeneration)
- API Routes
- Image Optimization
- Security Headers
- Advanced Error Boundaries
