Builds full-stack React applications with Next.js App Router, Server Components, Server Actions, and edge deployment. Use when creating Next.js projects, implementing routing, data fetching, caching, authentication, or deploying to Vercel.
Builds full-stack React applications with Next.js App Router, Server Components, Server Actions, and edge deployment.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/app-router.mdreferences/caching.mdreferences/deployment.mdreferences/server-actions.mdtemplates/layout.tsxtemplates/page.tsxtemplates/server-action.tsFull-stack React framework with App Router, Server Components, Server Actions, and optimized deployment patterns.
Create new project:
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
cd my-app && npm run dev
Essential file structure:
src/
app/
layout.tsx # Root layout (required)
page.tsx # Home page
globals.css # Global styles
api/ # Route handlers
components/ # React components
lib/ # Utilities
| File | Purpose |
|---|---|
page.tsx | Route UI |
layout.tsx | Shared UI wrapper |
loading.tsx | Loading UI (Suspense) |
error.tsx | Error boundary |
not-found.tsx | 404 UI |
route.ts | API endpoint |
app/
page.tsx # /
blog/
page.tsx # /blog
[slug]/
page.tsx # /blog/:slug
(marketing)/ # Route group (no URL segment)
about/page.tsx # /about
@modal/ # Parallel route (slot)
(.)photo/[id]/page.tsx # Intercepting route
Dynamic segments:
// app/blog/[slug]/page.tsx
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <Article slug={slug} />
}
Catch-all segments:
// app/docs/[...slug]/page.tsx - matches /docs/a, /docs/a/b, etc.
// app/docs/[[...slug]]/page.tsx - also matches /docs
// app/layout.tsx - Root layout (required)
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Nested layout:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
)
}
Default behavior - all components are Server Components unless marked with 'use client'.
// Server Component (default)
async function Posts() {
const posts = await db.posts.findMany() // Direct DB access
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
)
}
When to use Client Components:
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
// Direct fetch in Server Component
async function BlogPosts() {
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
return <PostList posts={posts} />
}
// With ORM
async function Users() {
const users = await prisma.user.findMany()
return <UserList users={users} />
}
export default async function Page() {
// Start both requests simultaneously
const postsPromise = getPosts()
const usersPromise = getUsers()
// Await both
const [posts, users] = await Promise.all([postsPromise, usersPromise])
return (
<>
<PostList posts={posts} />
<UserList users={users} />
</>
)
}
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
</div>
)
}
// app/actions.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
await db.posts.create({ data: { title, content } })
revalidatePath('/posts')
redirect('/posts')
}
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
)
}
'use server'
import { z } from 'zod'
const PostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
})
export async function createPost(formData: FormData) {
const validated = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
})
if (!validated.success) {
return { error: validated.error.flatten().fieldErrors }
}
await db.posts.create({ data: validated.data })
revalidatePath('/posts')
redirect('/posts')
}
'use client'
import { useActionState } from 'react'
import { createPost } from '@/app/actions'
export function CreatePostForm() {
const [state, action, pending] = useActionState(createPost, null)
return (
<form action={action}>
<input name="title" />
{state?.error?.title && <p>{state.error.title}</p>}
<button disabled={pending}>
{pending ? 'Creating...' : 'Create'}
</button>
</form>
)
}
// Cached by default (static)
const data = await fetch('https://api.example.com/data')
// Opt out of caching
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// Time-based revalidation
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1 hour
})
// Tag-based revalidation
const data = await fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function updatePost() {
// Revalidate specific path
revalidatePath('/posts')
// Revalidate by tag
revalidateTag('posts')
// Revalidate layout
revalidatePath('/posts', 'layout')
}
// Force dynamic rendering
export const dynamic = 'force-dynamic'
// Revalidate every 60 seconds
export const revalidate = 60
// Generate static params
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await db.posts.findMany()
return NextResponse.json(posts)
}
export async function POST(request: Request) {
const body = await request.json()
const post = await db.posts.create({ data: body })
return NextResponse.json(post, { status: 201 })
}
Dynamic route handler:
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const post = await db.posts.findUnique({ where: { id } })
if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json(post)
}
// middleware.ts (root level)
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 && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add headers
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
// Static metadata
export const metadata = {
title: 'My App',
description: 'App description',
openGraph: {
title: 'My App',
description: 'App description',
images: ['/og.png'],
},
}
// Dynamic metadata
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
}
}
import Image from 'next/image'
export function Avatar() {
return (
<Image
src="/avatar.png"
alt="Avatar"
width={64}
height={64}
priority // Above the fold
/>
)
}
// Remote images (configure in next.config.js)
<Image
src="https://example.com/image.jpg"
alt="Remote image"
width={800}
height={600}
/>
# .env.local (git ignored, local dev)
DATABASE_URL=postgresql://...
SECRET_KEY=abc123
# Public (exposed to browser)
NEXT_PUBLIC_API_URL=https://api.example.com
// Server only
const dbUrl = process.env.DATABASE_URL
// Client accessible
const apiUrl = process.env.NEXT_PUBLIC_API_URL
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default async function ProtectedPage() {
const cookieStore = await cookies()
const session = cookieStore.get('session')
if (!session) {
redirect('/login')
}
return <Dashboard />
}
// app/error.tsx
'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/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />
}
generateStaticParams - For static generation of dynamic routes| Mistake | Fix |
|---|---|
Using 'use client' everywhere | Only use for interactivity |
| Fetching in layout for child data | Fetch in the page/component that needs it |
Not awaiting params/searchParams | These are now Promises in Next.js 15 |
| Using API routes for mutations | Use Server Actions instead |
| Forgetting to revalidate cache | Call revalidatePath/revalidateTag |
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.