Skip to content
Sudip KC writing notes in a journal at a desk
SK.
← All articles

Development · · 6 min read

Why I Use Next.js for Most of My Modern Web Projects

  • Next.js
  • React
  • Architecture
  • Performance
  • Opinion

I don't reach for Next.js because it's popular. I reach for it because it answers the questions I'd otherwise spend the first week of every project arguing about: routing, rendering, data loading, images, and where the server code lives.

That said, "most" is doing real work in the title. There are projects where I don't use it, and I'll get to those. But for the majority of web products I build, from my own portfolio to client dashboards, Next.js 16 with the App Router is my default starting point.

Defaults I don't have to design

The biggest win is boring: decisions are already made. A new Next.js project gives me file-based routing, layouts that persist across navigation, a build pipeline, image optimisation, font loading, and metadata handling. I don't pick a router, configure a bundler, or write my own SSR setup.

That matters more when I'm leading a team. At NovaNext, a new developer can open an App Router project and know where things go. A page is in app/<route>/page.tsx. A layout wraps it. Loading and error UI sit next to it. The conventions do part of the onboarding for me.

app/
  layout.tsx          # root shell, fonts, providers
  page.tsx            # home
  blogs/
    page.tsx          # list
    [slug]/
      page.tsx        # article
      loading.tsx     # skeleton while it streams
      not-found.tsx   # bad slug
  work/
    page.tsx
lib/
  blogs.ts            # data access, no React
components/
  ui/                 # shared primitives

Compare that to a hand-rolled Vite + React Router setup. It's lighter, and I like it for small tools, but every team builds it slightly differently. Six months later, that difference is friction.

Server Components changed how I think about data

Before React Server Components, my data story in React apps was almost always the same: render a shell, fetch in an effect or with TanStack Query, show a spinner, render the result. It works, but the user waits twice: once for JavaScript, once for data.

With Server Components, a page can fetch on the server and send rendered HTML. No client bundle for the data-fetching code, no loading flash for content that could have been there from the start.

// app/blogs/page.tsx
import { getPublishedPosts } from "@/lib/blogs";
import { PostCard } from "@/components/post-card";

export default async function BlogsPage() {
  const posts = await getPublishedPosts();
  return (
    <section>
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </section>
  );
}

I still use TanStack Query heavily, but for a different job: client-side state that changes after the page loads, like a dashboard that polls, an order list that updates, or a form that mutates and refetches. Server Components handle the first paint. TanStack Query handles everything that's live. Keeping those two jobs separate has removed a lot of tangled code from my projects.

Most of the JavaScript I used to ship was there to fetch data the server already had.

It plays well with a Django backend

Many of my products have a Django REST Framework backend with PostgreSQL. People sometimes ask why I'd put Next.js in front of Django when Django can render templates itself.

The answer is that they do different jobs well. Django is excellent at models, admin, auth, permissions, and APIs. Next.js is excellent at building fast, interactive interfaces with good SEO. Server Components can call the Django API directly during render, so public pages come out as HTML, while authenticated dashboards behave like a client app. I covered how I lay out that split in How I Structure React + Django Projects.

Performance comes mostly for free

I care about performance because a lot of my users in Nepal are on mid-range Android phones and patchy mobile data. Next.js gives me a head start:

  • Automatic code splitting per route, so the blog page doesn't download the dashboard code.
  • `next/image` handles responsive sizes, lazy loading and modern formats without me writing a pipeline.
  • `next/font` self-hosts fonts and avoids layout shift.
  • Streaming with loading.tsx and Suspense means the shell shows up fast while slower parts fill in.
  • Static generation for content that doesn't change per user, like my blog posts.

None of that replaces actually measuring. But it means I start from a decent baseline instead of fixing basic problems later.

Turbopack made the dev loop fast again

On older versions, large projects had dev server start-up and hot reload times that broke my focus. With Turbopack as the default bundler in Next.js 16, the loop on my MacBook Air M4 feels instant for the projects I work on. That sounds minor until you count how many times a day you save a file.

Deployment options I actually use

I deploy Next.js two ways, depending on the project:

  • Vercel for my portfolio and marketing-style sites. Push to main, get a preview URL per branch, done.
  • A VPS with Nginx in front, for products where the backend already lives on that server or the client wants everything in one place. Next.js runs as a Node process, Nginx proxies to it, and Django runs under Gunicorn next door.

Being able to choose matters. I don't want my framework to lock me into one host, and in practice Next.js hasn't.

It grows with the project

My portfolio started as a set of pages. Then I added scroll-driven frame animations, a blog, and eventually turned it into a PWA with a service worker, offline page, install button and update toast. That last step is in I Turned My Portfolio Into a PWA — Here's How. At no point did I hit a wall where the framework stopped me. I just added what I needed.

The same goes for i18n, which I wrote about in Building a Multilingual Next.js Application. Route segments, layouts and server-side dictionaries fit together without hacks.

Where I don't use it

Next.js isn't the answer for everything, and pretending otherwise is how teams end up with overbuilt projects.

  • Mobile apps. Kanjiro is Flutter. I want native performance and offline-first storage, not a website in a wrapper.
  • Small internal tools with no SEO needs and a handful of users. A Vite SPA is simpler and has fewer moving parts.
  • Backend-heavy systems. I don't put business logic in Next.js route handlers when Django already owns the data and permissions. One source of truth for rules is worth more than one less service.
  • Pure static pages. If it's a single landing page with no interactivity, plain HTML is still fine.

The trade-offs I accept

It's not free. The App Router has a learning curve, especially the line between server and client components. Caching behaviour has changed between versions, and I've been caught by it. Major releases come with breaking changes, so I always read the upgrade guide on nextjs.org before bumping a version instead of assuming my old habits still apply.

There's also the risk of putting too much in the frontend just because you can. Server Actions are convenient, but on projects with a real backend I keep them thin, or skip them entirely.

Where I've landed

For anything that's a website or web app with real users, content that should be indexable, and a team that needs shared conventions, Next.js is my default. It gives me strong opinions where I want them and gets out of the way where I don't.

If you're choosing a stack, don't pick Next.js because I do. Pick it if you need what it's good at: server rendering, routing conventions, and a performance baseline you don't have to build yourself. If you don't need those things, something smaller will serve you better.