App Router in Next.js: Layout, Page und Route
1. Übersicht
App Router ist der Standard-Routing-Mechanismus von Next.js seit Version 13 und ersetzt schrittweise Pages Router (pages/). Er basiert auf einer Dateikonvention im app/-Verzeichnis: page.tsx erstellt eine Route, layout.tsx erstellt eine gemeinsam genutzte UI, route.ts erstellt einen API-Endpoint. Nach diesem Artikel wirst du wissen, wie du ein Next.js-Projekt mit App Router organisierst, verschachtelte Layouts schreibst, dynamische Routen und Route Handler erstellst und wie du diese Komponenten testest.
2. Grundstruktur des Verzeichnisses
App Router nutzt Ordner = URL-Segment. Jeder Ordner in app/ kann spezielle Dateien enthalten, die das Routing-Verhalten bestimmen.
app/
├── layout.tsx # Root Layout, muss <html>, <body> haben
├── page.tsx # Route "/"
├── about/
│ └── page.tsx # Route "/about"
├── products/
│ ├── layout.tsx # Layout nur für /products/*
│ ├── page.tsx # Route "/products"
│ └── [id]/
│ └── page.tsx # Route "/products/123"
└── api/
└── orders/
└── route.ts # API "/api/orders"
layout.tsxumhüllt allepage.tsxauf der gleichen Ebene oder tiefer.page.tsxist der tatsächliche Endpunkt, der Inhalte für eine URL rendert.route.tsrendert keine UI, sondern gibt eineResponseähnlich wie ein REST-Endpoint zurück.
Hinweis: Ordner, die nicht mit einem Kleinbuchstaben beginnen (z.B.
Products), funktionieren zwar, aber Next.js empfiehlt, durchgehend kebab-case oder camelCase zu verwenden, um Verwechslungen auf Betriebssystemen mit Groß-/Kleinschreibung zu vermeiden.
3. Page — Route definieren
Die Datei page.tsx exportiert eine React-Komponente als Haupt-UI für die Route. Standardmäßig ist sie eine Server Component.
// app/products/page.tsx
import { ProductService } from "@/services/product-service";
export default async function ProductsPage() {
const products = await ProductService.findAll();
return (
<main>
<h1>Produktliste</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name} - {product.price}đ</li>
))}
</ul>
</main>
);
}
async function ProductsPageermöglichtawaitvon Daten direkt in der Komponente, da dies eine Server Component ist – keinuseEffectodergetServerSidePropsnötig.- Next.js rendert HTML auf der Serverseite automatisch und sendet es bereits mit Inhalten zum Browser – gut für SEO und erste Anzeigzeit.
- Die Route entspricht der URL
/products.
3.1. Dynamische Route mit Parametern
// app/products/[id]/page.tsx
import { ProductService } from "@/services/product-service";
import { notFound } from "next/navigation";
type ProductPageProps = {
params: { id: string };
};
export default async function ProductDetailPage({ params }: ProductPageProps) {
const product = await ProductService.findById(params.id);
if (!product) {
notFound();
}
return <h1>{product.name}</h1>;
}
- Der Ordner
[id]erstellt ein dynamisches Segment, Next.js injiziert den tatsächlichen Wert inparams.id. notFound()rendert die nächstenot-found.tsx-Datei statt die App zum Absturz zu bringen – das ist der Standard-Weg zur Behandlung von 404ern im App Router.
4. Layout — gemeinsam genutzte UI
Layout empfängt den children-Prop für untergeordnete Routen und behält seinen State bei der Navigation zwischen Pages im gleichen Layout (z.B. wird die Sidebar nicht neu gerendert).
// app/products/layout.tsx
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="products-layout">
<aside>Produktfilter</aside>
<section>{children}</section>
</div>
);
}
childrenhier ist der Inhalt vonapp/products/page.tsxoderapp/products/[id]/page.tsx.- Beim Navigieren von
/productszu/products/123wird der<aside>-Bereich nicht unmountet/remountet – das ist der Hauptvorteil von Layout gegenüber dem manuellen Importieren einer gemeinsam genutzten Komponente in jeder Page.
4.1. Root Layout ist erforderlich
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Shop Nón Lá",
description: "Demo-Shop mit App Router",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="vi">
<body>
<header>Shop Nón Lá</header>
{children}
</body>
</html>
);
}
- Root Layout muss
<html>und<body>enthalten, da es das höchste Layout ist und die gesamte App umhüllt. export const metadataist der deklarative Weg, um<title>,<meta>zu deklarieren, Next.js injiziert es automatisch in<head>– kein manuelles DOM-Handling nötig.
Hinweis: Wenn du
app/layout.tsxvergisst, wird Next.js es beim Build sofort melden, da jede Route mindestens ein Root Layout benötigt.
5. Route Groups und Parallel Segments
Manchmal möchtest du Routen gruppieren, um Code zu organisieren, aber nicht, dass die Gruppe in der URL erscheint. Nutze Route Groups mit Klammern (name).
app/
├── (marketing)/
│ ├── layout.tsx # Layout nur für Marketing-Seiten
│ ├── page.tsx # Route "/"
│ └── pricing/
│ └── page.tsx # Route "/pricing"
└── (dashboard)/
├── layout.tsx # Layout nur für Dashboard
└── settings/
└── page.tsx # Route "/settings"
(marketing)und(dashboard)erscheinen nicht in der URL, sie helfen nur, verschiedene Layouts für verschiedene Seitengruppen zu verwenden./pricingund/settingsverwenden unterschiedliche Layouts, obwohl sie beide unterapp/liegen.
6. Route Handlers — APIs im App Router bauen
Die Datei route.ts ersetzt pages/api/*.ts von Pages Router und exportiert Funktionen nach HTTP-Methoden-Namen.
// app/api/orders/route.ts
import { NextRequest, NextResponse } from "next/server";
import { OrderService } from "@/services/order-service";
export async function GET() {
const orders = await OrderService.findAll();
return NextResponse.json(orders);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const order = await OrderService.create(body);
return NextResponse.json(order, { status: 201 });
}
GETundPOSTsind zwei separate Exports, Next.js routet Requests automatisch nach HTTP-Methode zur richtigen Funktion.NextResponse.json(...)setztContent-Type: application/jsonautomatisch, du brauchst nicht selbstJSON.stringify.- Dieser Endpoint wird unter
/api/ordersbereitgestellt und ist komplett unabhängig von der UI – es wird kein React gerendert.
6.1. Dynamischer Route Handler
// app/api/orders/[id]/route.ts
import { NextResponse } from "next/server";
import { OrderService } from "@/services/order-service";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const order = await OrderService.findById(params.id);
if (!order) {
return NextResponse.json({ message: "Order not found" }, { status: 404 });
}
return NextResponse.json(order);
}
- Wie dynamische Pages ermöglicht der Ordner
[id], den Wert aus der URL überparams.idzu erhalten. - Gib
status: 404explizit zurück, statt dass der Client den Fehler selbst erraten muss.
7. Spezielle Dateien: loading, error, not-found
App Router nutzt React Suspense und Error Boundary implizit durch Konventionsdateien, du brauchst nicht selbst einen Boundary zu schreiben.
// app/products/loading.tsx
export default function ProductsLoading() {
return <p>Produktliste wird geladen...</p>;
}
// app/products/error.tsx
"use client";
export default function ProductsError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<p>Fehler: {error.message}</p>
<button onClick={reset}>Erneut versuchen</button>
</div>
);
}
loading.tsxwird automatisch angezeigt, währendpage.tsxdes gleichen Segmentsawait-Daten aufbaut – keinuseState(isLoading)nötig.error.tsxmuss eine Client Component sein ("use client"), da esresetverwendet – eine Client-seitige Funktion zum Neu-Rendern des fehlerhaften Segments.- Next.js umhüllt Segments automatisch mit entsprechenden
<Suspense>und Error Boundaries basierend auf diesen Dateien.
8. Metadata und verschachtelte Layouts
Jedes Segment kann sein eigenes metadata exportieren, Next.js merged sie in Reihenfolge vom Root Layout zur Page.
// app/products/[id]/page.tsx
import type { Metadata } from "next";
import { ProductService } from "@/services/product-service";
type Props = { params: { id: string } };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await ProductService.findById(params.id);
return { title: product ? `${product.name} - Shop Nón Lá` : "Nicht gefunden" };
}
export default async function ProductDetailPage({ params }: Props) {
const product = await ProductService.findById(params.id);
return <h1>{product?.name}</h1>;
}
generateMetadataist eine dynamische Funktion, die es dir erlaubt,<title>basierend auf echten Produktdaten zu setzen – hilfreich für SEO auf Detailseiten.- Metadata von untergeordneten Pages überschreiben Felder mit gleichem Namen vom parent Layout, andere Felder bleiben bestehen.
9. Page-Komponenten testen
Bei asynchronen Server Components ist der einfachste Test-Weg, die Komponentenfunktion direkt aufzurufen und das zurückgegebene JSX zu prüfen, oder React Testing Library nach dem Rendern zu verwenden.
<!-- package.json (devDependencies, gekürzt) -->
<dependencies>
<vitest>^2.0.0</vitest>
<testing-library-react>^16.0.0</testing-library-react>
</dependencies>
// services/product-service.test.ts
import { describe, it, expect, vi } from "vitest";
import { ProductService } from "@/services/product-service";
import ProductsPage from "@/app/products/page";
import { render, screen } from "@testing-library/react";
vi.mock("@/services/product-service", () => ({
ProductService: {
findAll: vi.fn(),
},
});
describe("ProductsPage", () => {
it("zeigt die vom Service zurückgegebene Produktliste an", async () => {
(ProductService.findAll as any).mockResolvedValue([
{ id: "1", name: "Nón lá A", price: 50000 },
]);
const jsx = await ProductsPage();
render(jsx);
expect(screen.getByText("Nón lá A - 50000đ")).toBeInTheDocument();
});
});
ProductsPage()wird wie eine reine async Funktion aufgerufen, da sie grundlegend nur eine Funktion ist, die JSX zurückgibt – kein spezielles Test-Framework für Server Components nötig.vi.mockersetztProductService.findAlldurch einen Stub, der gefälschte Daten zurückgibt, sodass der Test nicht von der echten Datenbank abhängig ist.render(jsx)aus Testing Library prüft, dass das gerendertes JSX den erwarteten Inhalt im DOM korrekt darstellt.
10. Häufige Fehler und wann du es nicht verwenden solltest
- Vergessen von
"use client"beim Verwenden von Hooks wieuseState/useEffect: Server Components unterstützen diese Hooks nicht, Next.js wird einen klaren Build-Fehler anzeigen. - Sensible Datenabfrage-Logik (API-Schlüssel) in Client Components platzieren: Da der Code ins Browser-Bundle kommt. Bevorzuge immer Datenabfragen in Server Components oder Route Handlers.
- Route Groups missbrauchen nur für “schönere” Organisation: Wenn nicht wirklich verschiedene Layouts nötig sind, machen Route Groups die Verzeichnisstruktur nur unübersichtlicher.
pages/undapp/für die gleiche Route mischen: Die beiden Router können nicht die gleiche URL handhaben, Next.js bevorzugtapp/und ignoriert Routes inpages/– leicht zu Verwechslungen bei gradueller Migration.
Hinweis: Wenn dein Projekt mit Pages Router gut läuft und du Server Components, Streaming oder Server Actions nicht brauchst, ist eine sofortige Migration zum App Router nicht immer nötig. Bewerte die Vorteile, bevor du das komplette Routing umstellst.
11. Fazit
App Router organisiert eine Next.js-Anwendung durch drei Hauptdatei-Typen: page.tsx definiert Routes, layout.tsx teilt UI zwischen mehreren Routes, und route.ts baut APIs ohne HTML-Rendering. Kombiniert mit Konventionsdateien wie loading.tsx, error.tsx, not-found.tsx, kannst du geschmeidige Loading-/Error-Erlebnisse bauen, ohne Boundaries selbst zu schreiben. Beim Entwurf deiner Verzeichnisstruktur gruppiere nach wirklich gemeinsamem Layout, meide unnötige Route Groups und prüfe immer die Server/Client-Component-Grenzen, um sensible Logik nicht zum Browser zu leaken.