Next.js: When React Needs a Framework

SS Saurav Sitaula

React is a library, not a framework. I kept hearing that, but I didn't understand what it meant until my create-react-app project needed routing, SSR, API endpoints, and image optimization. Configuring everything myself was exhausting. Then Next.js entered the chat.

The Create-React-App Breaking Point

After adopting React Query, my data layer was solid. But everything else was held together with duct tape.

My project had:

  • React Router v6 (third migration, by the way)
  • A custom webpack config I was afraid to touch
  • An Express server I wrote for API routes
  • A hacky SSR setup that broke every other week
  • Manual code splitting with React.lazy
  • An image optimization pipeline involving three npm packages

My package.json had 47 dependencies. I could name maybe 30 of them.

One day, create-react-app printed a deprecation warning. The project was being sunset. I panicked. Then I realized: this was the push I needed.

”React Is a Library, Not a Framework”

I kept hearing this, but I didn’t truly get it until I listed everything React doesn’t give you:

  • Routing → React Router / TanStack Router
  • Server-side rendering → DIY with Express / custom setup
  • Data fetching → React Query / SWR / custom hooks
  • API routes → Express / Fastify / separate backend
  • Code splitting → Manual React.lazy + Suspense
  • Image optimization → sharp / custom pipeline
  • File-based routing → Nope
  • Metadata / SEO → react-helmet / custom
  • Build tooling → webpack / Vite / custom config
  • Deployment → Figure it out yourself

React gives you components and a rendering engine. Everything else is your problem.

A framework makes those decisions for you.

Enter: Next.js

Next.js is a React framework. It takes React and adds everything you’d eventually need anyway:

React alone:           React + Next.js:
├── You build routing   ├── File-based routing ✓
├── You build SSR       ├── SSR / SSG / ISR ✓
├── You build API       ├── API routes ✓
├── You optimize        ├── Image optimization ✓
├── You configure       ├── Zero-config ✓
└── You cry             └── You ship

File-Based Routing: The First “Aha”

In React Router, you define routes in code:

// React Router v6
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/blog" element={<Blog />} />
      <Route path="/blog/:slug" element={<BlogPost />} />
      <Route path="/dashboard" element={<Dashboard />}>
        <Route path="settings" element={<Settings />} />
        <Route path="profile" element={<Profile />} />
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

In Next.js, your file structure IS your routes:

app/
├── page.jsx              → /
├── about/
│   └── page.jsx          → /about
├── blog/
│   ├── page.jsx          → /blog
│   └── [slug]/
│       └── page.jsx      → /blog/:slug
├── dashboard/
│   ├── layout.jsx        → Shared dashboard layout
│   ├── settings/
│   │   └── page.jsx      → /dashboard/settings
│   └── profile/
│       └── page.jsx      → /dashboard/profile
└── not-found.jsx         → 404 page

No route configuration file. Create a file, get a route. Delete the file, route’s gone.

I migrated my 20-route app in an afternoon. With React Router, adding a new page was: create component, import it, add route, test. With Next.js: create file. Done.

Layouts: Nested and Persistent

My old app had a layout problem. Every page needed a navbar and sidebar, but I was wrapping them manually:

// Old way: every page repeats the layout
function DashboardSettings() {
  return (
    <DashboardLayout>   {/* Re-renders on every navigation */}
      <Settings />
    </DashboardLayout>
  );
}

function DashboardProfile() {
  return (
    <DashboardLayout>   {/* Re-renders again */}
      <Profile />
    </DashboardLayout>
  );
}

Next.js layouts persist between navigations:

// app/dashboard/layout.jsx
export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <Sidebar />   {/* Never re-mounts between dashboard pages */}
      <main>{children}</main>
    </div>
  );
}

// app/dashboard/settings/page.jsx
export default function Settings() {
  return <SettingsForm />;  // Only this part changes
}

// app/dashboard/profile/page.jsx  
export default function Profile() {
  return <ProfileEditor />;  // Sidebar stays mounted
}

Navigate between /dashboard/settings and /dashboard/profile? The sidebar stays. No re-mount. No flash. Instant transitions.

The Rendering Spectrum

This is where Next.js really clicked for me. It’s not just SSR. You choose the rendering strategy per page:

Static Site Generation (SSG)

Generated at build time. Perfect for content that doesn’t change often:

// app/blog/[slug]/page.jsx
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map(post => ({ slug: post.slug }));
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return <Article post={post} />;
}

Built once. Served from CDN. Instant load. This blog post? Static.

Server-Side Rendering (SSR)

Generated on every request. For dynamic, personalized content:

// app/dashboard/page.jsx
export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  const user = await getCurrentUser();
  const stats = await getUserStats(user.id);
  
  return <DashboardView user={user} stats={stats} />;
}

Fresh data on every request. Personalized per user.

Incremental Static Regeneration (ISR)

The best of both worlds. Static, but revalidates:

// app/products/[id]/page.jsx
export const revalidate = 3600;  // Regenerate every hour

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return <Product product={product} />;
}

First request builds the page. Serves from cache for an hour. After that, rebuilds in the background. Users always get fast loads. Data stays reasonably fresh.

API Routes: The Backend You Didn’t Know You Needed

Before Next.js, I had a separate Express server for my API:

// server.js — separate project
const express = require('express');
const app = express();

app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findOne({ id: req.params.id });
  res.json(user);
});

app.post('/api/posts', async (req, res) => {
  const post = await db.posts.create(req.body);
  res.json(post);
});

Two repos. Two deploys. Two sets of env variables. CORS configuration.

Next.js API routes live right next to your pages:

// app/api/users/[id]/route.js
export async function GET(request, { params }) {
  const user = await db.users.findOne({ id: params.id });
  return Response.json(user);
}

// app/api/posts/route.js
export async function POST(request) {
  const body = await request.json();
  const post = await db.posts.create(body);
  return Response.json(post);
}

Same repo. Same deploy. No CORS. API and frontend share types, utilities, and database connections.

Server Actions: Forms Without API Routes

Next.js 13+ introduced Server Actions. You can mutate data without writing an API endpoint:

// app/posts/new/page.jsx
async function createPost(formData) {
  'use server';
  
  const title = formData.get('title');
  const content = formData.get('content');
  
  await db.posts.create({ title, content });
  redirect('/posts');
}

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Write something..." required />
      <button type="submit">Publish</button>
    </form>
  );
}

No API route. No fetch call. No loading state management. The form just… works. Even without JavaScript.

When I first saw this, I thought about my Full Circle post. This is literally how PHP forms worked.

Image Optimization For Free

My old image pipeline:

// Before: manual optimization nightmare
import sharp from 'sharp';
// + custom loader
// + responsive images logic
// + lazy loading implementation
// + blur placeholder generation
// + WebP conversion

<img src="/images/hero.jpg" />  // 2MB unoptimized image

Next.js:

import Image from 'next/image';

<Image 
  src="/images/hero.jpg"
  width={800}
  height={600}
  alt="Hero image"
  placeholder="blur"
/>

Automatically:

  • Resizes to the right dimensions
  • Converts to WebP/AVIF
  • Lazy loads below the fold
  • Generates blur placeholder
  • Serves from CDN

One component. Zero configuration.

The Migration

Moving from create-react-app to Next.js wasn’t painless. Here’s what changed:

Before (CRA)After (Next.js)
React Router configFile-based routing
Custom SSR serverBuilt-in SSR
Express API serverAPI routes
Manual code splittingAutomatic
react-helmet for SEOBuilt-in metadata
Custom image pipelinenext/image
webpack.config.jsnext.config.js
47 dependencies23 dependencies

I deleted more code than I wrote. That’s usually a sign you’re doing something right.

The “Framework Lock-in” Concern

“But what about lock-in?” people asked.

Here’s my take: your React components are just React components. useQuery, useState, useEffect — those work anywhere. The Next.js-specific parts are routing and data fetching patterns.

If you ever leave Next.js, you’d need to:

  • Set up routing again
  • Set up SSR again
  • Set up API routes again

In other words, you’d need to rebuild everything Next.js gave you for free. That’s not lock-in. That’s value.

What I Wish I’d Known Earlier

  1. The App Router is the future. Pages Router still works, but new features go to App Router. Start there.

  2. Not everything needs SSR. Static pages are faster and cheaper. Use generateStaticParams for content-heavy pages.

  3. Server Components are the default. Add 'use client' only when you need interactivity. Start with the smallest client boundary.

  4. Middleware is powerful. Auth checks, redirects, A/B testing, geolocation — all at the edge, before your page even renders.

  5. next/image is non-negotiable. The performance difference is massive. Use it everywhere.

  6. Deploy to Vercel for the easiest path. But Next.js works on any Node.js host. Docker, AWS, self-hosted — all fine.

The Journey Continues

Next.js gave me the framework I didn’t know I was building myself, piece by painful piece. Routing, rendering, API routes, image optimization, SEO — all in one package.

But React itself kept evolving. New hooks. A compiler. A shift in how the community thinks about building apps.

Time to zoom out and look at where the React ecosystem landed.


P.S. — The day I deleted my webpack config, my Express server, and my custom SSR setup, I felt lighter. Like cleaning out a closet full of things you kept “just in case.” Turns out, I never needed the “just in case.” I needed a framework that handled it for me.

SS

Saurav Sitaula

Software Architect • Nepal

Back to all posts
Saurav.dev

© 2026 Saurav Sitaula.AstroNeoBrutalism