nextjs

SKILL.md

Next.js (App Router)

Quick Start

# Create new project
npx create-next-app@latest my-app --yes
cd my-app && npm run dev

Default setup: TypeScript, Tailwind, ESLint, App Router, Turbopack.

Core Concepts

File-System Routing

Routes are defined by folder structure in app/:

File Purpose
page.tsx Page UI (makes route public)
layout.tsx Shared UI wrapper
loading.tsx Loading skeleton (Suspense)
error.tsx Error boundary
not-found.tsx 404 UI
route.ts API endpoint

Server vs Client Components

Server Components (default):

  • Fetch data, access DB/secrets
  • Reduce JS bundle
  • No state/effects/browser APIs

Client Components ('use client'):

  • State, effects, event handlers
  • Browser APIs
  • Interactivity
// Server Component (default)
export default async function Page() {
    const data = await fetch('https://api.example.com/data')
    return <ClientButton data={data} />
}

// Client Component
'use client'
import { useState } from 'react'
export function ClientButton({ data }) {
    const [count, setCount] = useState(0)
    return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

Dynamic Routes

Pattern Example URL Usage
[slug] /blog/hello Single param
[...slug] /docs/a/b/c Catch-all
[[...slug]] /docs or /docs/a Optional catch-all
// app/blog/[slug]/page.tsx
export default async function Page({
    params
}: { params: Promise<{ slug: string }> }) {
    const { slug } = await params
    return <h1>{slug}</h1>
}

Route Groups & Private Folders

  • (group) - Organize without affecting URL
  • _folder - Private, not routable
app/
├── (marketing)/
│   └── page.tsx         → /
├── (dashboard)/
│   └── settings/
│       └── page.tsx     → /settings
└── _components/         → Not routable

Data Fetching

Server Components

// Direct fetch
export default async function Page() {
    const data = await fetch('https://api.example.com/data')
    return <div>{data.title}</div>
}

// With database
import { db } from '@/lib/db'
export default async function Page() {
    const posts = await db.select().from(posts)
    return <PostList posts={posts} />
}

Client Components

'use client'
import { use } from 'react'

// Via promise from server
export function Posts({ posts }: { posts: Promise<Post[]> }) {
    const data = use(posts)
    return <ul>{data.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}

Parallel Fetching

export default async function Page() {
    // Start both requests simultaneously
    const artistData = getArtist(id)
    const albumsData = getAlbums(id)
    
    const [artist, albums] = await Promise.all([artistData, albumsData])
    return <div>{artist.name}</div>
}

Caching & Revalidation

fetch Options

// Cache indefinitely
fetch(url, { cache: 'force-cache' })

// Revalidate every hour
fetch(url, { next: { revalidate: 3600 } })

// Tag for on-demand revalidation
fetch(url, { next: { tags: ['posts'] } })

use cache Directive

import { cacheTag, cacheLife } from 'next/cache'

export async function getProducts() {
    'use cache'
    cacheTag('products')
    cacheLife('hours')
    return db.query('SELECT * FROM products')
}

Revalidation

'use server'
import { revalidateTag, revalidatePath, updateTag } from 'next/cache'

export async function updateProduct() {
    await db.products.update(...)
    
    // Options:
    revalidateTag('products', 'max')  // Stale-while-revalidate
    updateTag('products')              // Immediate (Server Actions only)
    revalidatePath('/products')        // By path
}

Server Actions

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
    const title = formData.get('title')
    await db.post.create({ data: { title } })
    revalidateTag('posts')
}

// Usage in component
import { createPost } from './actions'

export function Form() {
    return (
        <form action={createPost}>
            <input name="title" />
            <button type="submit">Create</button>
        </form>
    )
}

Route Handlers (API)

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

export async function GET(request: NextRequest) {
    const searchParams = request.nextUrl.searchParams
    const query = searchParams.get('q')
    return Response.json({ data: [] })
}

export async function POST(request: Request) {
    const body = await request.json()
    return Response.json({ created: true }, { status: 201 })
}

// Dynamic route: app/api/posts/[id]/route.ts
export async function GET(
    request: Request,
    { params }: { params: Promise<{ id: string }> }
) {
    const { id } = await params
    return Response.json({ id })
}

Streaming & Suspense

import { Suspense } from 'react'

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

Or use loading.tsx for entire page streaming.

Components

Link

import Link from 'next/link'

<Link href="/dashboard">Dashboard</Link>
<Link href="/dashboard" replace>Replace history</Link>
<Link href="/dashboard" scroll={false}>Keep scroll</Link>
<Link href="/dashboard" prefetch={false}>No prefetch</Link>

Image

import Image from 'next/image'

// Local image (auto width/height)
import profilePic from './profile.png'
<Image src={profilePic} alt="Profile" />

// Remote image (explicit dimensions)
<Image
    src="https://example.com/photo.jpg"
    alt="Photo"
    width={500}
    height={300}
/>

// Fill container
<div style={{ position: 'relative', width: '100%', height: 300 }}>
    <Image src="/bg.jpg" alt="Background" fill style={{ objectFit: 'cover' }} />
</div>

Configure remote patterns in next.config.js:

module.exports = {
    images: {
        remotePatterns: [
            new URL('https://example.com/**')
        ]
    }
}

Metadata

// Static
export const metadata = {
    title: 'My Page',
    description: 'Page description'
}

// Dynamic
export async function generateMetadata({ params }) {
    const { id } = await params
    const product = await getProduct(id)
    return { title: product.name }
}

Additional Resources

Reference Links

Weekly Installs
3
First Seen
1 day ago
Installed on
opencode3
gemini-cli3
claude-code3
codex3
cursor3
github-copilot2