Next.js best practices expert. PROACTIVELY use when working with Next.js, App Router, Server Components, API routes. Triggers: nextjs, next.js, app router, server components, API routes
/plugin marketplace add nguyenthienthanh/aura-frog/plugin install aura-frog@aurafrogThis skill is limited to using the following tools:
Expert-level Next.js 14+ patterns, App Router, Server Components, data fetching, and optimization.
This skill activates when:
next in package.jsonapp/ directory)app/
├── (auth)/ # Route group (no URL impact)
│ ├── login/
│ │ └── page.tsx
│ └── register/
│ └── page.tsx
├── (dashboard)/
│ ├── layout.tsx # Shared dashboard layout
│ ├── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx # /dashboard/settings
├── api/
│ └── users/
│ └── route.ts # API route
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
└── global-error.tsx # Global error boundary
file_conventions[8]{file,purpose}:
page.tsx,Route UI component
layout.tsx,Shared layout (preserves state)
template.tsx,Shared layout (re-renders)
loading.tsx,Loading UI (Suspense)
error.tsx,Error boundary
not-found.tsx,404 page
route.ts,API endpoint
middleware.ts,Request middleware
// ✅ GOOD - Server Component (default, no directive needed)
// app/users/page.tsx
import { db } from '@/lib/db';
export default async function UsersPage() {
// Direct database access - runs on server only
const users = await db.user.findMany();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ✅ GOOD - Client Component (explicit directive)
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
// ✅ GOOD - Keep client boundary as low as possible
// app/dashboard/page.tsx (Server Component)
import { getUser } from '@/lib/auth';
import { UserProfile } from './UserProfile';
import { InteractiveChart } from './InteractiveChart'; // Client
export default async function DashboardPage() {
const user = await getUser();
return (
<div>
{/* Server Component - no JS shipped */}
<UserProfile user={user} />
{/* Client Component - only this ships JS */}
<InteractiveChart data={user.stats} />
</div>
);
}
// ✅ GOOD - Fetch in Server Component
async function getData() {
const res = await fetch('https://api.example.com/data', {
// Cache options
cache: 'force-cache', // Default - cached indefinitely
// cache: 'no-store', // No caching
// next: { revalidate: 60 }, // Revalidate every 60s
// next: { tags: ['posts'] }, // Tag-based revalidation
});
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{/* render data */}</div>;
}
// ✅ GOOD - Parallel fetching
export default async function Page() {
// Start both fetches simultaneously
const userPromise = getUser();
const postsPromise = getPosts();
// Wait for both
const [user, posts] = await Promise.all([userPromise, postsPromise]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
</div>
);
}
// ✅ GOOD - Stream slow data
import { Suspense } from 'react';
export default function Page() {
return (
<div>
{/* Renders immediately */}
<Header />
{/* Streams when ready */}
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
// Async Server Component
async function Posts() {
const posts = await getPosts(); // Slow fetch
return <PostList posts={posts} />;
}
// ✅ GOOD - Server Action in separate file
// 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 validatedFields = CreatePostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
const { title, content } = validatedFields.data;
await db.post.create({ data: { title, content } });
revalidatePath('/posts');
redirect('/posts');
}
// ✅ GOOD - Form with Server Action
// app/posts/new/page.tsx
import { createPost } from '@/app/actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<SubmitButton />
</form>
);
}
// Client component for pending state
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
);
}
'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';
export function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, null);
return (
<form action={formAction}>
<input name="title" />
{state?.errors?.title && <p>{state.errors.title}</p>}
<textarea name="content" />
{state?.errors?.content && <p>{state.errors.content}</p>}
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
</form>
);
}
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/users
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = searchParams.get('page') ?? '1';
const users = await db.user.findMany({
skip: (parseInt(page) - 1) * 10,
take: 10,
});
return NextResponse.json(users);
}
// POST /api/users
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 });
}
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
type Params = { params: { id: string } };
export async function GET(request: NextRequest, { params }: Params) {
const user = await db.user.findUnique({
where: { id: params.id },
});
if (user == null) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(user);
}
export async function DELETE(request: NextRequest, { params }: Params) {
await db.user.delete({ where: { id: params.id } });
return new NextResponse(null, { status: 204 });
}
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check auth
const token = request.cookies.get('token');
if (token == null && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set('x-request-id', crypto.randomUUID());
return response;
}
export const config = {
matcher: [
// Match all paths except static files
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
// app/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Home | My App',
description: 'Welcome to my app',
openGraph: {
title: 'Home | My App',
description: 'Welcome to my app',
images: ['/og-image.png'],
},
};
export default function HomePage() {
return <main>...</main>;
}
// app/posts/[slug]/page.tsx
import type { Metadata } from 'next';
type Props = { params: { slug: string } };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
export default async function PostPage({ params }: Props) {
const post = await getPost(params.slug);
return <article>{/* ... */}</article>;
}
cache_strategies[4]{strategy,use_case,code}:
Static,Rarely changes,cache: 'force-cache'
Time-based,Updates periodically,next: { revalidate: 60 }
On-demand,User-triggered,revalidatePath() / revalidateTag()
No cache,Always fresh,cache: 'no-store'
// app/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updatePost(id: string, data: PostData) {
await db.post.update({ where: { id }, data });
// Revalidate specific path
revalidatePath(`/posts/${id}`);
// Or revalidate by tag
revalidateTag('posts');
}
// In fetch
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
import Image from 'next/image';
// ✅ GOOD - Optimized image
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Load immediately (LCP)
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
// ✅ GOOD - Fill container
<div className="relative h-64 w-full">
<Image
src={post.coverImage}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
</div>
// ✅ GOOD - Remote images (configure in next.config.js)
// next.config.js
module.exports = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.example.com' },
],
},
};
// app/error.tsx (Client Component required)
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/global-error.tsx (Root layout errors)
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
checklist[12]{pattern,best_practice}:
Components,Server by default Client when needed
Client directive,'use client' at top of file
Data fetching,Fetch in Server Components
Parallel fetch,Promise.all for multiple fetches
Streaming,Suspense for slow data
Forms,Server Actions + useFormStatus
API routes,Route handlers in app/api
Caching,Tag-based revalidation
Images,next/image with sizes prop
Metadata,generateMetadata for dynamic
Errors,error.tsx at route level
Loading,loading.tsx for Suspense
Version: 1.3.0
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.