AgentSkillsCN

nextjs-app-router

引导 Next.js App Router 的模式,包括路由分组、Provider 架构、认证中间件、轻量级页面约定,以及布局组合。适用于创建页面、布局、路由处理器、中间件,或配置 Provider 时使用。

SKILL.md
--- frontmatter
name: nextjs-app-router
description: "Guides Next.js App Router patterns including route groups, provider architecture, middleware for auth, thin page conventions, and layout composition. Activates when creating pages, layouts, route handlers, middleware, or configuring providers."

Next.js App Router Patterns

This project uses Next.js 16 App Router with route groups, a layered provider architecture, and auth middleware.

IMPORTANT: New feature pages ALWAYS go in src/app/(dashboard)/[feature]/page.tsx. Never create pages outside of the route groups.

Route Groups

The app uses route groups to separate layouts without affecting URLs:

Route GroupLayoutPurposeProviders
(auth)Centered card, no navSign-in pagesNone (inherits root)
(dashboard)AppShell (sidebar + topbar)All authenticated pagesTheme, FeatureFlags, Datadog, Tooltip, Toaster

New feature pages always go in (dashboard)/[feature]/page.tsx.

Decision Tree: Where Does This Page Go?

code
Is this an authentication page (sign-in, sign-up, reset password)?
├── YES → src/app/(auth)/[page]/page.tsx
│         (centered card layout, no navigation)
└── NO → Is this an authenticated app page?
    ├── YES → src/app/(dashboard)/[feature]/page.tsx
    │         (gets AppShell + sidebar + topbar + all dashboard providers)
    └── NO → Is this an API endpoint?
        ├── YES → src/app/api/[resource]/route.ts
        └── NO → Is this a root-level page (home, 404)?
            └── YES → src/app/page.tsx or src/app/not-found.tsx

Provider Architecture

Providers are layered — root providers wrap everything, dashboard providers are scoped:

code
Root layout (src/app/layout.tsx)
└── RootProviders (src/lib/providers/root-providers.tsx)
    ├── SessionProvider (NextAuth)
    └── QueryClientProvider (TanStack Query)
        └── (dashboard) layout
            └── DashboardProviders (src/lib/providers/dashboard-providers.tsx)
                ├── ThemeProvider (next-themes)
                ├── FeatureFlagProvider (src/services/launchdarkly/)
                ├── DatadogProvider (src/services/datadog/)
                ├── TooltipProvider (Radix)
                ├── AppShell (Sidebar + Topbar + content)
                └── Toaster (sonner)

See examples/root-providers.tsx and examples/dashboard-providers.tsx for the implementations.

Provider Rules

  • All provider logic lives in src/lib/providers/
  • Layouts only import and compose — no provider configuration inline
  • Dashboard providers gracefully degrade when env vars are missing
  • New cross-cutting providers go in src/lib/providers/[name]-provider.tsx
  • External service providers (FeatureFlags, Datadog) live in src/services/[name]/, not lib/providers/
  • Providers that need the session (FeatureFlags, Datadog) go in dashboard, not root

Middleware

Route protection via NextAuth middleware in middleware.ts:

  • All routes are protected by default
  • Unauthenticated users redirect to /signin
  • Authenticated users on /signin redirect to /dashboard
  • Auth API routes (/api/auth/) pass through

See examples/middleware.ts for the implementation.

Thin Page Convention

Pages in (dashboard)/ are async Server Components that:

  1. Call await prefetchXxx() for SSR data
  2. Wrap children with <Hydrate>
  3. Render feature components
typescript
// ✅ CORRECT — thin page
// src/app/(dashboard)/tasks/page.tsx
import { Hydrate } from "@/lib/hydrate";
import { TaskList, prefetchTasks } from "@/features/tasks";

export default async function TasksPage() {
  await prefetchTasks();
  return (
    <Hydrate>
      <TaskList />
    </Hydrate>
  );
}

No business logic in pages. Pages are glue between the framework and features.

CRUD Routing Convention

Entity CRUD uses Intercepting Routes + Parallel Routes:

  • Create: Modal via @modal/(.)create/page.tsx (intercepted route)
  • Create fallback: Full page create/page.tsx (direct URL / hard refresh)
  • Detail: Full page [id]/page.tsx
  • Edit: Full page [id]/edit/page.tsx
  • @modal/default.tsx MUST render null
  • Modal only for simple create forms. Complex forms use full pages.
  • Form components live in the feature, not in app/.
typescript
// ❌ WRONG — page with logic, direct fetching, inline rendering
export default async function TasksPage() {
  const res = await fetch("https://api.example.com/tasks");
  const tasks = await res.json();
  const active = tasks.filter((t: any) => t.status === "active");
  return (
    <div>
      <h1>Tasks</h1>
      {active.map((t: any) => <p key={t.id}>{t.title}</p>)}
    </div>
  );
}

API Route Handlers

Located in src/app/api/[resource]/route.ts:

  • Named exports per HTTP method (GET, POST, PUT, DELETE)
  • Use ApiResponse<T> envelope from @/types/api
  • Validate request bodies with Zod .safeParse()
  • Proper HTTP status codes
  • try/catch error handling
typescript
// src/app/api/tasks/route.ts
import { NextRequest } from "next/server";
import { taskSchema, createTaskInputSchema } from "@/features/tasks";
import type { ApiResponse } from "@/types/api";
import type { Task } from "@/features/tasks";

export async function GET(): Promise<Response> {
  try {
    const tasks = await db.tasks.findMany();
    return Response.json({
      data: tasks,
      error: null,
      meta: { total: tasks.length },
    } satisfies ApiResponse<Task[]>);
  } catch {
    return Response.json(
      { data: null, error: { message: "Failed to fetch tasks", code: "FETCH_ERROR" } },
      { status: 500 }
    );
  }
}

export async function POST(req: NextRequest): Promise<Response> {
  try {
    const body = await req.json();
    const result = createTaskInputSchema.safeParse(body);

    if (!result.success) {
      return Response.json(
        { data: null, error: { message: "Validation failed", code: "VALIDATION_ERROR" } },
        { status: 400 }
      );
    }

    const task = await db.tasks.create({ data: result.data });
    return Response.json({ data: task, error: null } satisfies ApiResponse<Task>, { status: 201 });
  } catch {
    return Response.json(
      { data: null, error: { message: "Failed to create task", code: "CREATE_ERROR" } },
      { status: 500 }
    );
  }
}

Layout Conventions

  • Root layout (src/app/layout.tsx): Fonts + RootProviders
  • Dashboard layout (src/app/(dashboard)/layout.tsx): DashboardProviders + AppShell
  • Auth layout (src/app/(auth)/layout.tsx): Centered card layout
  • Default exports only for page.tsx and layout.tsx

Error Handling Convention Files

The feedback feature provides state components used in Next.js convention files:

Convention FileComponentPurpose
error.tsxErrorStateError boundary UI
not-found.tsxNotFoundState404 page UI
loading.tsxLoadingStateLoading state UI
typescript
// src/app/(dashboard)/error.tsx
"use client";
import { ErrorState } from "@/features/feedback";

export default function DashboardError({ error, reset }: { error: Error; reset: () => void }) {
  return <ErrorState error={error} onRetry={reset} />;
}

Available UI State Components

The feedback feature provides state components for all scenarios:

  • ErrorState — generic error with retry action
  • NotFoundState — 404
  • ForbiddenState — 403
  • ServerErrorState — 500
  • LoadingState — spinner
  • EmptyState — no data (MUST be used instead of returning null)
  • WipState — work in progress
  • PendingState — processing

"use client" Decision Tree

code
Does this component use any of these?
├── React hooks (useState, useEffect, useContext, etc.) → "use client"
├── Event handlers (onClick, onChange, onSubmit, etc.) → "use client"
├── Browser APIs (window, document, localStorage, etc.) → "use client"
├── Third-party client libraries (useQuery, useRouter from next/navigation, etc.) → "use client"
└── None of the above → Server Component (no directive needed)

DO NOT

  • DO NOT create pages outside of route groups — use (auth)/ or (dashboard)/.
  • DO NOT put business logic in page files — pages are thin (prefetch + Hydrate + render).
  • DO NOT put provider configuration directly in layout files — use src/lib/providers/.
  • DO NOT use default export for components — only page.tsx and layout.tsx get default exports.
  • DO NOT modify shadcn/ui files in src/components/ui/ — use them as-is.
  • DO NOT add "use client" to Server Components — only add it when the component uses hooks, event handlers, or browser APIs.
  • DO NOT return null for empty states — use EmptyState from @/features/feedback.
  • DO NOT put API logic directly in pages — create route handlers in src/app/api/.
  • DO NOT forget error handling in route handlers — always use try/catch with ApiResponse<T>.
  • DO NOT forget Zod validation in POST/PUT route handlers — always validate request bodies.