Claude Code Plugins

Community-maintained marketplace

Feedback

Next.js 15+ App Router development patterns including Server Components, Client Components, data fetching, layouts, and server actions. Use when creating pages, routes, layouts, components, API route handlers, server actions, loading states, error boundaries, or working with Next.js navigation and metadata.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name nextjs
displayName Next.js
description Next.js 15+ App Router development patterns including Server Components, Client Components, data fetching, layouts, and server actions. Use when creating pages, routes, layouts, components, API route handlers, server actions, loading states, error boundaries, or working with Next.js navigation and metadata.
version 1.0.0

Next.js Development Guidelines

Development patterns for Next.js 15+ using the App Router, Server Components, and modern data fetching.

Core Principles

  1. Server-First Architecture: Default to Server Components, use Client Components only when needed
  2. File-Based Routing: Use App Router conventions for pages, layouts, and route handlers
  3. Data Fetching: Fetch data where it's needed using async/await in Server Components
  4. Type Safety: Leverage TypeScript for route params, search params, and data types
  5. Performance: Optimize with streaming, parallel data fetching, and static generation

App Router Structure

File Conventions

app/
├── layout.tsx              # Root layout (required)
├── page.tsx               # Home page
├── loading.tsx            # Loading UI
├── error.tsx              # Error boundary
├── not-found.tsx          # 404 page
├── posts/
│   ├── layout.tsx         # Posts layout
│   ├── page.tsx          # /posts
│   ├── [id]/
│   │   └── page.tsx      # /posts/123
│   └── new/
│       └── page.tsx      # /posts/new
└── api/
    └── posts/
        └── route.ts      # API route handler

Page Component

// app/posts/page.tsx
import { getPosts } from '@/lib/api';

export const metadata = {
  title: 'Posts',
  description: 'Browse all blog posts'
};

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <a href={`/posts/${post.id}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

Dynamic Routes

// app/posts/[id]/page.tsx
import { getPost } from '@/lib/api';
import { notFound } from 'next/navigation';

interface PageProps {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export async function generateMetadata({ params }: PageProps) {
  const { id } = await params;
  const post = await getPost(id);
  return {
    title: post.title,
    description: post.excerpt
  };
}

export default async function PostPage({ params }: PageProps) {
  const { id } = await params;
  const post = await getPost(id);

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Server vs Client Components

Server Components (Default)

Use for:

  • Data fetching
  • Accessing backend resources
  • Keeping sensitive info on server
  • Reducing client-side JavaScript
// app/posts/page.tsx (Server Component by default)
import { db } from '@/lib/db';

export default async function PostsPage() {
  // Direct database access
  const posts = await db.post.findMany();

  return <PostList posts={posts} />;
}

Client Components

Use for:

  • Event listeners (onClick, onChange, etc.)
  • State and lifecycle (useState, useEffect)
  • Browser-only APIs
  • Custom hooks
// components/SearchBar.tsx
'use client'; // Required directive

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export function SearchBar() {
  const [query, setQuery] = useState('');
  const router = useRouter();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    router.push(`/search?q=${query}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search posts..."
      />
      <button type="submit">Search</button>
    </form>
  );
}

Composition Pattern

// app/posts/page.tsx (Server Component)
import { getPosts } from '@/lib/api';
import { SearchBar } from '@/components/SearchBar'; // Client Component

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <div>
      <SearchBar /> {/* Client Component for interactivity */}
      <PostList posts={posts} /> {/* Can be Server Component */}
    </div>
  );
}

Data Fetching

Basic Pattern

// Server Component with async/await
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Cache for 1 hour
  }).then(res => res.json());

  return <PostList posts={posts} />;
}

Parallel Data Fetching

export default async function DashboardPage() {
  // Fetch in parallel
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats()
  ]);

  return (
    <div>
      <UserProfile user={user} />
      <PostList posts={posts} />
      <Stats data={stats} />
    </div>
  );
}

Streaming with Suspense

import { Suspense } from 'react';

export default function PostsPage() {
  return (
    <div>
      <h1>Posts</h1>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
    </div>
  );
}

async function Posts() {
  const posts = await getPosts(); // Slow data fetch
  return <PostList posts={posts} />;
}

Layouts

Root Layout (Required)

// app/layout.tsx
import './globals.css';

export const metadata = {
  title: {
    default: 'My Blog',
    template: '%s | My Blog'
  }
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>{/* Navigation */}</nav>
        </header>
        <main>{children}</main>
        <footer>{/* Footer */}</footer>
      </body>
    </html>
  );
}

Nested Layout

// app/posts/layout.tsx
export default function PostsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <aside>
        <PostsSidebar />
      </aside>
      <div>{children}</div>
    </div>
  );
}

Route Handlers (API Routes)

Basic Handler

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const posts = await getPosts();
  return NextResponse.json({ posts });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const post = await createPost(body);
  return NextResponse.json({ post }, { status: 201 });
}

Dynamic Route Handler

// app/api/posts/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const post = await getPost(params.id);

  if (!post) {
    return NextResponse.json(
      { error: 'Post not found' },
      { status: 404 }
    );
  }

  return NextResponse.json({ post });
}

Server Actions

Basic Server Action

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

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

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  const post = await db.post.create({
    data: { title, content }
  });

  revalidatePath('/posts');
  redirect(`/posts/${post.id}`);
}

Using in Forms

import { createPost } from '@/app/actions/posts';

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

Navigation

Link Component

import Link from 'next/link';

export function PostCard({ post }: { post: Post }) {
  return (
    <Link href={`/posts/${post.id}`} prefetch={true}>
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
    </Link>
  );
}

Programmatic Navigation

'use client';

import { useRouter } from 'next/navigation';

export function PostActions({ postId }: { postId: string }) {
  const router = useRouter();

  const handleDelete = async () => {
    await deletePost(postId);
    router.push('/posts');
    router.refresh(); // Refresh server components
  };

  return <button onClick={handleDelete}>Delete</button>;
}

Router Hooks

'use client';

import { usePathname, useSearchParams } from 'next/navigation';

const pathname = usePathname(); // /posts/123
const searchParams = useSearchParams(); // ?q=hello
const query = searchParams.get('q');

Metadata

// Static metadata
export const metadata = {
  title: 'All Posts',
  description: 'Browse our collection of blog posts'
};

// Dynamic metadata
export async function generateMetadata({ params }: PageProps) {
  const { id } = await params;
  const post = await getPost(id);
  return {
    title: post.title,
    description: post.excerpt
  };
}

Error Handling

// app/posts/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>
  );
}

// app/posts/[id]/not-found.tsx
export default function NotFound() {
  return <div>Post Not Found</div>;
}

Static Generation & ISR

// Generate static pages at build time
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ id: post.id }));
}

// Revalidate every hour (ISR)
export const revalidate = 3600;

export default async function PostPage({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params;
  const post = await getPost(id);
  return <Post data={post} />;
}

Additional Resources

For detailed information, see: