HiveCore Dev logo hivecore.dev

App Router vs Pages Router in 2026

// Next.js · HiveCore Dev · updated 2026-05-09
// what's in here
  1. The short version
  2. Working example
  3. Why this pattern
  4. A common variant
  5. Trade-offs to watch
  6. A more involved example
  7. When to skip it
  8. FAQ

TL;DR: Pages Router still ships. App Router has the future. Here's when to use each — and when migration is worth the pain.

The short version

Pages Router still ships. App Router has the future. Here's when to use each — and when migration is worth the pain.

This guide covers the mental model, the patterns that pay off, and the trade-offs that decide whether a technique fits your code.

Working example

Here's a minimal example you can run as-is. Drop it in a fresh file, run it, and trace through it once before reading the rest.

import { useState } from "react";

export function Counter({ initial = 0 }: { initial?: number }) {
  const [n, setN] = useState(initial);
  return (
    <button onClick={() => setN((v) => v + 1)}>
      count: {n}
    </button>
  );
}

Why this pattern

The shape above shows up in real Next.js codebases because it satisfies three constraints at once: it stays type-safe, it composes with the rest of the language's idioms, and it leaves a clear trail for the next developer (which, in six months, is you).

When you write the same pattern three times in a project, extract it. When you write it three times across projects, extract it into a shared library.

// recommended — vercel Vercel — zero-config Next.js hosting, generous free tier

A common variant

The same idea adapted for a different shape. Notice how the structure stays the same — only the specifics change.

// app/posts/page.tsx — RSC, no "use client"
async function getPosts(): Promise<{ id: string; title: string }[]> {
  const r = await fetch("https://api.example.com/posts", {
    next: { revalidate: 60 },
  });
  return r.json();
}

export default async function PostsPage() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

Trade-offs to watch

Every pattern has a failure mode. The most common one here is over-application: developers who learn a technique apply it everywhere, including places where simpler code would have been clearer.

Rule of thumb: if the abstraction takes more lines to describe than it saves, the abstraction is wrong.

A more involved example

Once the basic pattern is clear, here's how it composes with surrounding code. Read this one slowly.

"use server";

import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
  const title = formData.get("title");
  if (typeof title !== "string" || title.length < 1) {
    return { error: "title required" };
  }
  await db.post.create({ data: { title } });
  revalidatePath("/posts");
  return { ok: true };
}

When to skip it

If the surrounding code is already simple, don't reach for Next.js-specific cleverness. Boring code is a feature. Save the patterns for places where they actually pay off — usually at module boundaries, in shared libraries, or where the alternative would be 50 lines of repetition.

// recommended — cloudflare Cloudflare Pages — alternative Next.js host with edge-first networking

FAQ

Is this still current in 2026?

Yes. The patterns shown here are stable across recent versions and reflect what working teams actually ship.

Where do I learn more?

Read the official docs first, then the source of a project you respect. Tutorials get you to the door; source code gets you inside.

Does this work for production?

The exact code in this article is illustrative — copy the shape, adapt the specifics. For production, add logging, add tests, handle the failure modes called out above.

Related reading