Skip to content

Next.js App Router and RSC — Building Full Applications

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Next.js App Router and RSC. We cover key concepts, practical examples, and best practices to help you master this topic.

Next.js App Router is the primary framework for React Server Components, providing file-system routing, layouts, loading states, error boundaries, and data revalidation.

What You'll Learn

You will understand how RSC work within the App Router, how to use layouts, loading files, error boundaries, and route groups with Server and Client Components.

Why It Matters

The App Router is where most developers use RSC in production. Understanding its conventions is essential for building real applications.

Real-World Use

DodaTech's entire tutorial platform runs on Next.js App Router with RSC, using layouts for the site chrome, loading.tsx for streaming skeletons, and error.tsx for graceful error handling.

flowchart TD
    A[app/] --> B[layout.js]
    A --> C[page.js]
    A --> D[loading.js]
    A --> E[error.js]
    A --> F[not-found.js]
    B --> G[Root Layout]
    G --> H[Route Group]
    H --> I[layout.js]
    I --> J[page.js]
    I --> K[loading.js]
    I --> L[error.js]
    style A fill:#1e293b,color:#fff
    style G fill:#0f172a,color:#fff

Folder Structure and File Conventions

The App Router uses a file-system based routing convention with special files for different concerns.

// app/layout.js — Root layout (Server Component by default)
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <header>Site Header</header>
        <main>{children}</main>
        <footer>Site Footer</footer>
      </body>
    </html>
  );
}

// app/page.js — Home page
export default async function HomePage() {
  const featured = await db.posts.findFeatured();
  return (
    <div>
      <h1>Welcome</h1>
      {featured.map(p => <PostCard key={p.id} post={p} />)}
    </div>
  );
}

// app/posts/[id]/page.js — Dynamic route
export default async function PostPage({ params }) {
  const post = await db.posts.findById(params.id);
  return <Article post={post} />;
}

Expected output: The root layout wraps every page. The home page fetches featured posts. The dynamic route renders individual posts based on the URL parameter.

Layouts with Server Components

Layouts are Server Components by default and persist across navigations. They fetch shared data once.

// app/dashboard/layout.js
import { Suspense } from 'react';
import { Sidebar } from './Sidebar';
import { Navbar } from './Navbar';

export default async function DashboardLayout({ children }) {
  const user = await getCurrentUser();
  const notifications = await db.notifications.findUnread(user.id);

  return (
    <div style={{ display: 'flex' }}>
      <Sidebar user={user} notifications={notifications} />
      <div style={{ flex: 1 }}>
        <Navbar user={user} />
        <Suspense fallback={<p>Loading page...</p>}>
          {children}
        </Suspense>
      </div>
    </div>
  );
}

Expected output: The dashboard layout fetches user data and notifications once. The sidebar and navbar render with this data. Child pages inside the dashboard render within the Suspense boundary.

Loading UI with loading.js

Create loading.tsx files to show immediate loading UI while Server Components fetch data.

// app/posts/loading.js
export default function PostsLoading() {
  return (
    <div>
      <div style={{ width: '60%', height: '32px', background: '#f0f0f0', marginBottom: '16px' }} />
      {[1, 2, 3, 4, 5].map(i => (
        <div key={i} style={{ padding: '16px', margin: '8px 0', border: '1px solid #eee' }}>
          <div style={{ width: '80%', height: '24px', background: '#f0f0f0', marginBottom: '8px' }} />
          <div style={{ width: '40%', height: '16px', background: '#f0f0f0' }} />
        </div>
      ))}
    </div>
  );
}

Expected output: When navigating to /posts, the loading skeleton displays immediately while the Server Component fetches data. Once data is ready, the skeleton is replaced with the actual content.

Error Handling with error.js

Error boundaries catch errors in Server and Client Components and display fallback UI.

'use client';
// app/posts/error.js
export default function PostsError({ error, reset }) {
  return (
    <div style={{ padding: '24px', textAlign: 'center' }}>
      <h2>Something went wrong loading posts</h2>
      <p style={{ color: '#666' }}>{error.message}</p>
      <button onClick={reset} style={{ padding: '8px 16px', marginTop: '16px' }}>
        Try Again
      </button>
    </div>
  );
}

Expected output: If any Server or Client Component in the posts route throws an error, the error boundary catches it and displays a friendly message with a retry button.

Route Groups and Organization

Use route groups (folders in parentheses) to organize routes without affecting the URL.

// app/(marketing)/page.js — / (home page)
// app/(marketing)/about/page.js — /about
// app/(marketing)/contact/page.js — /contact

// app/(dashboard)/dashboard/page.js — /dashboard
// app/(dashboard)/dashboard/settings/page.js — /dashboard/settings
// app/(dashboard)/dashboard/analytics/page.js — /dashboard/analytics

Expected output: Route groups organize the file structure. The URL paths are not affected by the group folder names. Each group can have its own layout.js.

Common Mistakes

  1. Putting use client in layout.js unnecessarily: Most layout logic (data fetching, structure) does not need interactivity. Only add 'use client' if the layout uses hooks or browser APIs.

  2. Forgetting loading.js for pages with async components: Without loading.js, the page blocks until data is ready. Always add loading.js for pages with async Server Components.

  3. Not using error.js for production apps: Unhandled errors crash the page. error.js provides a graceful fallback and retry mechanism.

  4. Putting too many parallel routes in one layout: Each layout fetches its own data. Deeply nested layouts can create waterfalls. Keep the layout tree shallow.

  5. Using client-side navigation when Server Components handle routing: Next.js handles client-side navigation automatically. Server Components re-fetch data as needed during navigation.

Practice Questions

  1. What is the purpose of layout.js in the App Router?

Layouts wrap child pages and persist across navigations. They fetch shared data and provide consistent UI around the page content.

  1. How does loading.js improve user experience?

It shows immediate fallback UI while the page's Server Components fetch data, preventing blank screens during navigation.

  1. Where should error boundaries be placed?

At each route segment that can fail independently. Each error.js file catches errors in its segment and all child segments.

  1. What are route groups and why use them?

Route groups are folders in parentheses that organize routes without affecting URLs. They allow different layouts for different sections.

  1. Can layouts be async Server Components?

Yes. Layouts can be async and fetch data. The fetched data is available to the layout and its children.

Challenge

Create an App Router structure for a SaaS application with: a marketing section (home, about, blog) with a shared header, a dashboard section (overview, analytics, settings) with a sidebar layout, each section has its own loading and error states.

Frequently Asked Questions

Can I have multiple root layouts?

No. Only one root layout.js is allowed. Use route groups with different layouts for different sections of your app.

Does the App Router support static export?

Yes. Use output: 'export' in next.config.js. However, Server Components run at build time for static export, not at request time.

How do middleware and RSC interact?

Middleware runs before the request reaches the App Router. It can redirect, rewrite, or add headers that Server Components can read.

Can I use the Pages Router alongside the App Router?

Yes. Both routers can coexist in the same project. This is useful for incremental Migration from Pages to App Router.

Does the App Router support i18n routing?

Yes. Use the middleware for locale detection and the route groups pattern for localized routes. Next.js provides built-in i18n support.

Mini Project

Build a multi-section app with a marketing layout (header + footer), a dashboard layout (sidebar + navbar), loading skeletons for each section, error boundaries, and route groups to organize the file structure.

What's Next

Learn about RSC Directives for a complete reference on use client and use server.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro