AgentSkillsCN

nextjs

Next.js 14+ 框架的防护措施、设计模式与最佳实践,助力 AI 辅助开发。 适用于 Next.js 项目开发,或当用户提及 Next.js/Next 时使用。 提供应用路由模式、服务器组件、数据获取以及部署指南。

SKILL.md
--- frontmatter
name: nextjs
description: |
  Next.js 14+ framework guardrails, patterns, and best practices for AI-assisted development.
  Use when working with Next.js projects, or when the user mentions Next.js/Next.
  Provides App Router patterns, server components, data fetching, and deployment guidelines.
license: MIT
metadata:
  author: samuel
  version: "1.0"
  category: framework
  language: typescript
  extensions: ".jsx,.tsx"

Next.js Framework Guide

Framework: Next.js 14+ (App Router) Language: TypeScript/JavaScript Use Cases: Full-Stack Web Apps, SSR/SSG, E-commerce, Blogs, Dashboards

Overview

Next.js is a React framework providing server-side rendering, static site generation, API routes, and full-stack development in a single codebase. Version 14+ uses the App Router as the default, built on React Server Components.

Project Setup

bash
# Create new Next.js app
npx create-next-app@latest my-app --typescript --tailwind --eslint --app

cd my-app
npm run dev

Recommended Project Structure

code
my-app/
├── app/
│   ├── (auth)/                 # Route group (no URL segment)
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   ├── dashboard/
│   │   ├── page.tsx            # /dashboard
│   │   ├── loading.tsx         # Loading UI
│   │   ├── error.tsx           # Error boundary
│   │   └── layout.tsx          # Dashboard layout
│   ├── api/
│   │   └── users/route.ts     # API route handler
│   ├── globals.css
│   ├── layout.tsx              # Root layout (required)
│   └── page.tsx                # Home page (/)
├── components/
│   ├── ui/                     # Reusable UI components
│   └── features/               # Feature-specific components
├── lib/
│   ├── db.ts                   # Database client
│   └── utils.ts                # Utility functions
├── hooks/                      # Custom React hooks
├── types/                      # TypeScript type definitions
├── public/                     # Static assets
├── middleware.ts               # Edge middleware
├── next.config.js
├── tailwind.config.ts
└── package.json

Routing (App Router)

File-Based Routing Conventions

FilePurpose
page.tsxRoute UI (makes segment publicly accessible)
layout.tsxShared layout (wraps children, persists)
loading.tsxLoading UI (Suspense boundary)
error.tsxError boundary (must be 'use client')
not-found.tsx404 UI for this segment
route.tsAPI route handler (GET, POST, etc.)
template.tsxLike layout but re-mounts on navigation
default.tsxFallback for parallel routes

Route Patterns

code
app/
├── page.tsx                    # /
├── about/page.tsx              # /about
├── blog/
│   ├── page.tsx                # /blog
│   └── [slug]/page.tsx         # /blog/:slug (dynamic)
├── shop/
│   └── [...categories]/page.tsx  # /shop/a/b/c (catch-all)
├── (marketing)/                # Route group (no URL impact)
│   ├── pricing/page.tsx        # /pricing
│   └── features/page.tsx       # /features
└── @modal/                     # Parallel route (named slot)
    └── login/page.tsx

Page Component with Params

tsx
// app/blog/[slug]/page.tsx
interface PageProps {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}

export default function BlogPost({ params, searchParams }: PageProps) {
  return <article><h1>Post: {params.slug}</h1></article>;
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const post = await getPost(params.slug);
  return { title: post.title, description: post.excerpt };
}

Layouts

tsx
// app/layout.tsx -- Root Layout (required, wraps entire app)
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: { default: 'My App', template: '%s | My App' },
  description: 'My application',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}

Nested layouts compose automatically. Dashboard layout wraps all /dashboard/* routes:

tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <Sidebar />
      <div className="flex-1">{children}</div>
    </div>
  );
}

Server Components vs Client Components

Decision Rule

NeedComponent Type
Fetch data, access backend resourcesServer (default)
Static rendering, SEO contentServer
Use hooks (useState, useEffect, etc.)Client
Browser APIs (window, localStorage)Client
Event handlers (onClick, onChange)Client
Third-party client-only librariesClient

Server Component (Default)

All components in the app/ directory are Server Components by default. They run on the server only and can directly access databases, file systems, and secrets.

tsx
// app/users/page.tsx -- Server Component (no directive needed)
import { db } from '@/lib/db';

export default async function UsersPage() {
  const users = await db.user.findMany();
  return (
    <ul>
      {users.map((user) => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Client Component

Add 'use client' at the top of the file. Push this directive as low in the tree as possible.

tsx
// components/Counter.tsx
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

Composition Pattern

Fetch data in Server Components, pass to Client Components as props:

tsx
// app/dashboard/page.tsx (Server Component)
import { ClientSidebar } from '@/components/ClientSidebar';
import { db } from '@/lib/db';

export default async function Dashboard() {
  const stats = await db.stats.get();
  return (
    <div>
      <ClientSidebar initialStats={stats} />
      <DashboardContent stats={stats} />
    </div>
  );
}

Data Fetching

Server Component Fetch with Caching

tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // ISR: revalidate every hour
  });
  if (!res.ok) throw new Error('Failed to fetch products');
  return res.json();
}

Fetch Caching Options

OptionBehavior
{ cache: 'force-cache' }Static (default for GET)
{ cache: 'no-store' }Dynamic (no caching)
{ next: { revalidate: N } }ISR (revalidate every N seconds)
{ next: { tags: ['posts'] } }Tag-based revalidation

Parallel Fetching

Always fetch independent data in parallel with Promise.all:

tsx
export default async function Dashboard({ params }: { params: { id: string } }) {
  const [user, orders] = await Promise.all([
    getUser(params.id),
    getOrders(params.id),
  ]);
  return <div><UserProfile user={user} /><OrderList orders={orders} /></div>;
}

Streaming with Suspense

tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <WelcomeMessage />
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

Server Actions

Define mutations with 'use server'. They run on the server and can be called from forms or client code.

tsx
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';

const createPostSchema = z.object({
  title: z.string().min(1),
  content: z.string().min(10),
});

export async function createPost(formData: FormData) {
  const validated = createPostSchema.parse({
    title: formData.get('title'),
    content: formData.get('content'),
  });
  await db.post.create({ data: validated });
  revalidatePath('/posts');
  redirect(`/posts`);
}

Use in a form (no client JavaScript required for basic submissions):

tsx
export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create</button>
    </form>
  );
}

API Route Handlers

tsx
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

export async function GET(request: NextRequest) {
  const page = parseInt(request.nextUrl.searchParams.get('page') || '1');
  const users = await db.user.findMany({ skip: (page - 1) * 10, take: 10 });
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validated = userSchema.parse(body);
    const user = await db.user.create({ data: validated });
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json({ errors: error.errors }, { status: 400 });
    }
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}

Middleware

Runs at the edge before every matched request. Use for auth checks, redirects, headers.

tsx
// middleware.ts (project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value;
  const isProtected = request.nextUrl.pathname.startsWith('/dashboard');

  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Error Handling

Error Boundary (error.tsx)

tsx
// app/dashboard/error.tsx
'use client';

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <h2>Something went wrong</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Not Found

tsx
// app/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <Link href="/">Return Home</Link>
    </div>
  );
}

Trigger programmatically: import { notFound } from 'next/navigation'; notFound();

Configuration

js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }],
  },
  async redirects() {
    return [{ source: '/old', destination: '/new', permanent: true }];
  },
  async headers() {
    return [{
      source: '/api/:path*',
      headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }],
    }];
  },
};
module.exports = nextConfig;

Guardrails

  • Use Server Components by default; add 'use client' only when needed
  • Push 'use client' as low in the component tree as possible
  • Colocate data fetching with the component that needs it
  • Use Promise.all for independent parallel fetches
  • Implement loading.tsx and error.tsx for every route segment
  • Use Server Actions for mutations (not API routes for form submissions)
  • Validate all inputs with schema validators (Zod) in Server Actions and API routes
  • Use next/image for images and next/font for fonts (performance)
  • Set proper metadata on every page for SEO
  • Use Suspense boundaries to stream slow data
  • Never import server-only modules in Client Components
  • Never expose secrets or database access in Client Components

Commands Reference

bash
npm run dev          # Development server (http://localhost:3000)
npm run build        # Production build
npm run start        # Start production server
npm run lint         # ESLint check
npx tsc --noEmit     # TypeScript validation
npm test             # Run tests (Vitest/Jest)

Advanced Topics

For detailed code examples, advanced patterns, testing strategies, performance optimization, caching strategies, and ISR/SSG/SSR details, see:

  • references/patterns.md -- Authentication, advanced Server Actions, testing, performance, caching, rendering strategies, deployment

External References