Skip to content
⚠️ This article was written in 2022. Some content may be outdated.

Next.js 13 App Router: Redefining Full-Stack Development

Next.js 13 was released, and the biggest change is the App Router (/app directory), which takes a completely different approach from Pages Router.

App Router vs Pages Router ​

pages/(旧)          app/(新)
├── index.tsx         ├── page.tsx        ← 页面组件
├── about.tsx         ├── about/
├── api/              │   └── page.tsx
│   └── users.ts      ├── layout.tsx      ← 布局(新概念)
└── _layout.tsx       ├── loading.tsx     ← 加载状态(新概念)
                      ├── error.tsx       ← 错误边界(新概念)
                      └── api/
                          └── users/
                              └── route.ts ← API 路由

Server Components (Core Changes) ​

tsx
// app/users/page.tsx
// 默认是 Server Component(服务端执行,不打包到客户端)
async function UsersPage() {
  // 直接 async/await,不需要 getServerSideProps
  const users = await db.user.findMany(); // 直接访问数据库!

  return (
    <div>
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

export default UsersPage;
tsx
// app/users/UserCard.tsx
// "use client" 声明客户端组件(有交互、有 hooks)
"use client";

import { useState } from "react";

export function UserCard({ user }: { user: User }) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div onClick={() => setIsExpanded(!isExpanded)}>
      <h2>{user.name}</h2>
      {isExpanded && <p>{user.bio}</p>}
    </div>
  );
}

Key principle: A Server Component can contain a Client Component, but not the other way around.

layout.tsx: Nested Layouts ​

tsx
// app/layout.tsx(根布局)
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <body>
        <Navigation />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}

// app/dashboard/layout.tsx(嵌套布局)
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar />
      <div className="flex-1">{children}</div>
    </div>
  );
}
// 访问 /dashboard/settings:RootLayout → DashboardLayout → SettingsPage

New Data Fetching Approaches ​

tsx
// 扩展 fetch:自动缓存
async function getData() {
  // 默认缓存(静态生成)
  const staticData = await fetch("https://api.example.com/posts");

  // 每次请求重新获取(服务端渲染)
  const dynamicData = await fetch("https://api.example.com/users", {
    cache: "no-store",
  });

  // 60 秒重新验证(增量静态再生成)
  const revalidatedData = await fetch("https://api.example.com/config", {
    next: { revalidate: 60 },
  });
}

Route Handlers (API Routes) ​

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = Number(searchParams.get("page")) || 1;

  const users = await db.user.findMany({
    skip: (page - 1) * 20,
    take: 20,
  });

  return NextResponse.json({ users, page });
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  const user = await db.user.create({ data: body });
  return NextResponse.json(user, { status: 201 });
}

Metadata API (SEO) ​

tsx
// app/products/[id]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({ params }): Promise<Metadata> {
  const product = await getProduct(params.id);

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      images: [{ url: product.image }],
    },
  };
}

My Assessment ​

App Router is a paradigm-level revolution: Server Components + streaming make "server-first" possible.

But it wasn't mature when first released in November 2022:

  • Documentation was incomplete
  • Many third-party libraries didn't support Server Components
  • Debugging was complex (the server/client boundary isn't easy to understand)

Recommendation: new projects can start using it; for existing projects, wait 6 months for the ecosystem to stabilize before migrating.

Summary ​

  • App Router defaults to Server Components: use async/await directly, not bundled to the client
  • 'use client' declares components that need interactivity and hooks
  • layout.tsx nested layouts are more flexible than _app.js
  • fetch extends caching control with three strategies (static / dynamic / incremental regeneration)

MIT Licensed