Next.js App Router Best Practices: Migration from Pages Router

ContentQR Team
6 min read
Technical Development
Next.js
App Router
Migration
Best Practices

The Next.js App Router represents a significant evolution in React-based web development, introducing powerful features like React Server Components, improved data fetching, and better developer experience. As you consider migrating from Pages Router or start a new project with App Router, understanding best practices becomes essential for building efficient, scalable applications. This comprehensive guide shares ContentQR's migration experience, best practices, and lessons learned during the transition. You'll learn about the key benefits of App Router, migration strategies, common patterns, and how to leverage new features effectively. Whether you're planning a migration or starting fresh with App Router, these insights will help you build better applications with improved performance and developer experience. We'll explore practical examples, migration tips, and best practices that you can apply immediately to your Next.js projects.

Why Migrate to App Router?

The App Router introduces several significant improvements:

  • React Server Components: Better performance and reduced client-side JavaScript
  • Improved Data Fetching: Built-in support for async components
  • Better Layouts: Nested layouts with shared UI
  • Streaming: Progressive page rendering for better UX
  • Type Safety: Better TypeScript support

Migration Strategy

1. Gradual Migration Approach

We didn't migrate everything at once. Instead, we:

  1. Created new features in App Router
  2. Migrated high-traffic pages first
  3. Gradually moved remaining pages
  4. Kept API routes compatible during transition

2. File Structure Changes

Before (Pages Router):

pages/
  ├── index.tsx
  ├── about.tsx
  └── api/
      └── users.ts

After (App Router):

app/
  ├── page.tsx
  ├── about/
  │   └── page.tsx
  └── api/
      └── users/
          └── route.ts

Key Differences and Best Practices

Server Components by Default

In App Router, components are Server Components by default. This means:

// ✅ Server Component (default)
export default async function BlogPage() {
  const posts = await getAllPosts(); // Direct data fetching
  return <BlogList posts={posts} />;
}

// ❌ Client Component (needs 'use client')
'use client';
export default function InteractiveButton() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Data Fetching Patterns

Server Components:

// app/blog/page.tsx
export default async function BlogPage() {
  const posts = await getAllPosts(); // Direct fetch, no useEffect needed
  return <BlogList posts={posts} />;
}

Client Components:

'use client';
import { useEffect, useState } from 'react';

export function BlogList() {
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(setPosts);
  }, []);
  
  return <div>{/* render posts */}</div>;
}

Layout Patterns

Nested layouts are powerful in App Router. When working with layouts and routing, you may also need to implement middleware for authentication and logging. Learn more about Next.js middleware chain design to handle cross-cutting concerns effectively.

Nested layouts are powerful in App Router:

// app/layout.tsx - Root layout
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Navbar />
        {children}
        <Footer />
      </body>
    </html>
  );
}

// app/blog/layout.tsx - Blog-specific layout
export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <BlogSidebar />
      {children}
    </div>
  );
}

Common Pitfalls and Solutions

1. Mixing Server and Client Components

Problem: Trying to use hooks in Server Components

Solution: Use 'use client' directive only when needed

// ✅ Correct
'use client';
export function ClientComponent() {
  const [state, setState] = useState();
  // ...
}

// ❌ Wrong - hooks in Server Component
export function ServerComponent() {
  const [state, setState] = useState(); // Error!
}

2. Metadata Generation

Before:

// pages/blog/[slug].tsx
export function getStaticProps() {
  return { props: { metadata: {...} } };
}

After:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
  };
}

3. Route Handlers vs API Routes

Before:

// pages/api/users.ts
export default function handler(req, res) {
  res.json({ users: [...] });
}

After:

// app/api/users/route.ts
export async function GET() {
  return Response.json({ users: [...] });
}

Performance Optimizations

App Router's performance optimizations work together with React Server Components. Learn more about Server Components vs Client Components and React 19 new features to maximize your application's performance.

1. Static Generation

// Generate static params at build time
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map(post => ({ slug: post.slug }));
}

2. Streaming and Suspense

import { Suspense } from 'react';

export default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <AsyncContent />
    </Suspense>
  );
}

3. Image Optimization

import Image from 'next/image';

// Automatic optimization
<Image
  src="/image.jpg"
  width={800}
  height={600}
  alt="Description"
  priority // For above-the-fold images
/>

Lessons Learned

  1. Start Small: Migrate one page at a time
  2. Test Thoroughly: Server Components behave differently
  3. Use TypeScript: Better type safety helps catch errors
  4. Monitor Performance: Track Core Web Vitals during migration
  5. Document Changes: Keep team informed of new patterns

Conclusion

Migrating to App Router was a significant improvement for ContentQR. The performance gains, better developer experience, and improved type safety made the effort worthwhile. While the migration required some learning and adjustment, the benefits far outweigh the initial challenges. The key is to approach migration systematically, learn the new patterns gradually, and leverage App Router's features effectively.

Key Takeaways:

  • App Router provides better performance with React Server Components
  • Improved data fetching patterns simplify async operations
  • Better developer experience with improved type safety and tooling
  • Migration requires learning new patterns but delivers significant benefits
  • Start with small features and gradually expand your App Router usage

If you're considering migrating to App Router, start with a small feature, learn the patterns, and gradually expand. The investment in learning App Router will pay off in better performance and developer experience. Remember that migration is a process - take time to understand the new patterns, and don't rush the transition.

Next Steps:

  • Evaluate your current Pages Router application and identify migration opportunities
  • Start with a small feature to learn App Router patterns
  • Gradually migrate features one at a time
  • Leverage App Router features like Server Components and improved data fetching
  • Document your migration experience and best practices for your team