nextjs

mgd34msu's avatarfrom mgd34msu

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.

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 4, 2026

When & Why to Use This Skill

This Claude skill enables the rapid development of high-performance, full-stack React applications using the Next.js App Router. It streamlines the implementation of modern web patterns such as Server Components, Server Actions, and Edge deployment, ensuring your projects follow industry best practices for speed, SEO, and scalability.

Use Cases

  • Scaffolding new Next.js projects with a standardized directory structure, TypeScript, and Tailwind CSS integration.
  • Implementing complex routing architectures including dynamic segments, parallel routes, and intercepting routes for advanced UI patterns.
  • Building secure and efficient data-driven features using Server Components for fetching and Server Actions for mutations.
  • Optimizing application performance through granular caching strategies, streaming with Suspense, and Image optimization.
  • Configuring SEO-friendly metadata and middleware for authentication, redirects, and header management.
namenextjs
descriptionBuilds 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.

Next.js

Full-stack React framework with App Router, Server Components, Server Actions, and optimized deployment patterns.

Quick Start

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

App Router Fundamentals

File Conventions

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

Routing Patterns

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

Layouts

// 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>
  )
}

Server Components

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:

  • Event handlers (onClick, onChange)
  • State and lifecycle (useState, useEffect)
  • Browser APIs
  • Custom hooks with state
'use client'

import { useState } from 'react'

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

Data Fetching

// 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} />
}

Parallel Data Fetching

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} />
    </>
  )
}

Streaming with Suspense

import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

Server Actions

Basic Form

// 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>
  )
}

With Validation

'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')
}

With useActionState

'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>
  )
}

Caching

Data Cache

// 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'] }
})

Revalidation

'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')
}

Route Segment Config

// 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 }))
}

API Routes (Route Handlers)

// 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

// 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*'],
}

Metadata & SEO

// 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,
  }
}

Image Optimization

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}
/>

Environment Variables

# .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

Common Patterns

Authentication Check

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 />
}

Error Handling

// 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>
  )
}

Loading States

// app/dashboard/loading.tsx
export default function Loading() {
  return <DashboardSkeleton />
}

Best Practices

  1. Default to Server Components - Only use Client Components when needed
  2. Colocate data fetching - Fetch data in the component that needs it
  3. Use Server Actions for mutations - Not API routes
  4. Implement proper loading states - Use Suspense and loading.tsx
  5. Configure caching appropriately - Don't over-cache dynamic content
  6. Use generateStaticParams - For static generation of dynamic routes

Common Mistakes

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

Reference Files

Templates