Reviewing Next.js as a Java Programmer

I came to Next.js from years of Java, and my first reaction was frustration. Java is verbose and has to be compiled, but in the end it’s predictable. Next.js is verbose and syntactically strange and hides an enormous amount of behavior behind the scenes — and it still has to be compiled. After fighting it for a while, I think the friction is real, but most of it comes from a handful of concepts that have no clean Java equivalent. Here’s my honest review. ☕

The part that actually breaks your brain: where does this code run?

In Java, your code runs on the Java Virtual Machine (JVM). Period. In Next.js (App Router), a single file might run on the server, in the browser, or during the build — and the rules differ for each. By default a component renders on the server. Add a click handler and you get an explicit build-time error telling you event handlers can’t live in a Server Component. To opt that file into the browser you add a magic string at the very top:

1
2
3
4
5
'use client';

export default function LikeButton() {
  return <button onClick={() => alert('hi')}>Like</button>;
}

One nuance worth correcting, because it tripped me up: a Client Component is not browser-only. It still renders once on the server during Server-Side Rendering (SSR), then again in the browser. “Client” means “also ships to the client,” not “runs only on the client.” Once that clicked, half my confusion went away.

Magic file names instead of configuration

Spring Boot’s @GetMapping(“/route”) is annotation magic, but it’s explicit — the route lives next to the method. Next.js routes by file name instead. Name a file page.tsx and it becomes a page; layout.tsx wraps it; route.ts becomes an HTTP endpoint. Typo the name and the route silently disappears, with no compiler complaint. Powerful once you internalize it, unnerving until you do.

JSX mixes what Java keeps apart

Java separates logic from presentation. JSX deliberately fuses them — an asynchronous database call sitting inside what looks like an HTML template, with JavaScript’s ternary and && operators standing in for if/else:

1
2
3
4
5
6
7
8
9
10
export default async function Dashboard() {
  const users = await db.getUsers(); // server-side query
  return (
    <div>
      {users.length === 0
        ? <p>No users found</p>
        : users.map(u => <UserCard key={u.id} name={u.name} />)}
    </div>
  );
}

It looks messy to a Java eye, but it’s deliberate: the component is the controller and the view at once. Fight that and you’ll be miserable; accept it and it’s surprisingly compact. 💡

The caching claim everyone repeats is out of date

You’ll read that Next.js “aggressively overrides fetch and caches everything by default.” That was true in Next.js 13 and 14, and people hated it. Next.js 15 reversed it: fetch requests, GET route handlers, and the client router cache are no longer cached by default — you opt in now. If you’re learning today, ignore the old horror stories. (Also: it overrides fetch on the server, not the browser’s native fetch.)

Compiling the uncompilable

One thing the internet routinely garbles: the Rust-based compiler that transforms your code is SWC; Turbopack is the Rust-based bundler. They’re two different tools, and neither one is what does server-rendering or hydration — that’s the React runtime. When a Java compile fails, the stack trace points at a line. When Next.js fails, you often get a cryptic “hydration mismatch” because the server-rendered HTML didn’t match what the browser produced. That error class is the single biggest “why is this happening” moment for newcomers.

A translation table for your Java brain

What finally made it tolerable was mapping each concept to something I already knew:

Next.js / React Rough Java / Spring equivalent
Server Component A controller that renders a view
Client Component (‘use client’) A browser script that also pre-renders on the server
page.tsx A @GetMapping(“/route”)
layout.tsx A master template / layout decorator
Props Constructor arguments

Verdict

Next.js is not unpredictable because it’s badly designed — it’s unpredictable because it asks one file to live in three environments, and Java never had to. The verbosity is real, the magic is real, and the error messages can be genuinely worse than a Java stack trace. But once you stop expecting JVM-style “what you see is what runs” and accept the server/client split as the core mental model, the rest stops feeling like chaos. Coming from Java, you already think in layers — you just have to learn which layer a given line of code is standing on. 🎉

This entry was posted in java, javascript and tagged , , , . Bookmark the permalink.

Comments are closed.