Next.js App Router: A One-Year Review

Dev
Β·Dante Chun

Why I Switched to App Router

In early 2023, when Next.js 13 officially released the App Router, I honestly hesitated. I was already running several projects with Pages Router, and the thought "why bother changing?" was strong. But I had a new project starting, and I decided to apply the latest technology.

One year later, I don't regret that decision.

What Actually Worked Well

1. The Power of Server Components

Initially, I thought "what's really different from client components?" But in practice, I realized how much simpler the code becomes when you can directly fetch and render data on the server.

// Before (Pages Router + API Route)
// pages/api/posts.ts
export default async function handler(req, res) {
  const posts = await prisma.post.findMany()
  res.json(posts)
}

// pages/posts.tsx
export default function Posts() {
  const { data } = useSWR('/api/posts')
  if (!data) return <Loading />
  return <PostList posts={data} />
}
// After (App Router)
// app/posts/page.tsx
export default async function PostsPage() {
  const posts = await prisma.post.findMany()
  return <PostList posts={posts} />
}

No need to create separate API routes, no need to manage loading states manually. Just fetch the data and render.

2. The Layout System

In Pages Router, you had to handle common layouts in _app.tsx or use getLayout on each page. App Router's layout.tsx solves this elegantly.

app/
β”œβ”€β”€ layout.tsx        # Global layout
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ layout.tsx    # Blog-specific layout
β”‚   β”œβ”€β”€ page.tsx
β”‚   └── [slug]/
β”‚       └── page.tsx
└── dashboard/
    β”œβ”€β”€ layout.tsx    # Dashboard-specific layout
    └── page.tsx

The nested layout structure makes applying different UIs per section much more natural.

3. Loading and Error Handling

loading.tsx and error.tsx are genuinely convenient. Before, I had to manually manage loading/error states on every page. Now, just create one file and it's handled automatically.

// app/blog/loading.tsx
export default function Loading() {
  return <BlogSkeleton />
}

The Honest Struggles

1. Caching Complexity

Next.js's caching system is powerful, but it was really confusing at first. The default behavior of fetch being cached, revalidate options, router.refresh()... I spent a long time figuring out why my data wasn't updating.

Now I organize it like this:

  • Static data: Use default caching
  • Frequently changing data: { cache: 'no-store' } or revalidate: 0
  • Periodic refresh: { next: { revalidate: 3600 } }
  • On-demand refresh: revalidatePath() or revalidateTag()

2. Client/Server Component Boundaries

Where to put "use client", why this hook doesn't work in server components - I had to learn one by one through error messages.

My established principles:

  • Start with server components by default
  • Split into client components when you need state, event handlers, or browser APIs
  • Keep client components as small as possible

For example, this blog's article list is server-rendered, but the infinite scroll feature is separated into a client component.

3. Handling Metadata

Unlike Pages Router's next/head, App Router uses generateMetadata. It felt unfamiliar at first, but it's actually more intuitive for generating dynamic metadata.

export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    description: post.description,
    openGraph: { images: [post.thumbnail] },
  }
}

From a Solo Developer's Perspective

As someone developing alone, the biggest advantage of App Router is reduced code. Creating separate API routes, fetching from the client, managing loading states... all this boilerplate is significantly reduced.

Of course, there's a learning curve. For the first 3 months, my productivity was actually lower than with Pages Router. But after getting comfortable, I can implement features much faster.

I use App Router for client projects too. I don't need to explain to clients that "server components reduce bundle size," but being able to build faster websites in less time is definitely an advantage.

Recommended Learning Path

For those just starting with App Router, here's my recommended learning order:

  1. Understand the server component concept
  2. Learn layout and page structure
  3. Data fetching methods (direct fetch in server components)
  4. Understand when to split into client components
  5. Establish caching and revalidation strategies
  6. Utilize Server Actions

Conclusion

After using App Router for a year, my takeaway is "the long-term gains outweigh the initial investment."

It's definitely harder than Pages Router at first. But once you're comfortable, you can create better results with less code. Especially for solo developers, having less code to maintain is meaningful in itself.

If you're starting a new project, I recommend App Router. However, if you already have projects running smoothly on Pages Router, there's no need to migrate. Next.js supports both routers simultaneously, so gradually transitioning when adding new features is also a good approach.