Learn-skills.dev nextjs
Guide for implementing Next.js - a React framework for production with server-side rendering, static generation, and modern web features. Use when building Next.js applications, implementing App Router, working with server components, data fetching, routing, or optimizing performance.
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/aia-11-hn-mib/mib-mockinterviewaibot/nextjs" ~/.claude/skills/neversight-learn-skills-dev-nextjs-d51293 && rm -rf "$T"
data/skills-md/aia-11-hn-mib/mib-mockinterviewaibot/nextjs/SKILL.mdNext.js Skill
Next.js is a React framework for building full-stack web applications with server-side rendering, static generation, and powerful optimization features built-in.
Reference
https://nextjs.org/docs/llms.txt
When to Use This Skill
Use this skill when:
- Building new Next.js applications (v15+)
- Implementing App Router architecture
- Working with Server Components and Client Components
- Setting up routing, layouts, and navigation
- Implementing data fetching patterns
- Optimizing images, fonts, and performance
- Configuring metadata and SEO
- Setting up API routes and route handlers
- Migrating from Pages Router to App Router
- Deploying Next.js applications
Core Concepts
App Router vs Pages Router
App Router (Recommended for v13+):
- Modern architecture with React Server Components
- File-system based routing in
directoryapp/ - Layouts, loading states, and error boundaries
- Streaming and Suspense support
- Nested routing with layouts
Pages Router (Legacy):
- Traditional page-based routing in
directorypages/ - Uses
,getStaticProps
,getServerSidePropsgetInitialProps - Still supported for existing projects
Key Architectural Principles
- Server Components by Default: Components in
are Server Components unless marked withapp/'use client' - File-based Routing: File system defines application routes
- Nested Layouts: Share UI across routes with layouts
- Progressive Enhancement: Works without JavaScript when possible
- Automatic Optimization: Images, fonts, scripts auto-optimized
Installation & Setup
Create New Project
npx create-next-app@latest my-app # or yarn create next-app my-app # or pnpm create next-app my-app # or bun create next-app my-app
Interactive Setup Prompts:
- TypeScript? (Yes recommended)
- ESLint? (Yes recommended)
- Tailwind CSS? (Optional)
directory? (Optional)src/- App Router? (Yes for new projects)
- Import alias? (Default: @/*)
Manual Setup
npm install next@latest react@latest react-dom@latest
package.json scripts:
{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" } }
Project Structure
my-app/ ├── app/ # App Router (v13+) │ ├── layout.tsx # Root layout │ ├── page.tsx # Home page │ ├── loading.tsx # Loading UI │ ├── error.tsx # Error UI │ ├── not-found.tsx # 404 page │ ├── global.css # Global styles │ └── [folder]/ # Route segments ├── public/ # Static assets ├── components/ # React components ├── lib/ # Utility functions ├── next.config.js # Next.js configuration ├── package.json └── tsconfig.json
Routing
File Conventions
- Page UI for routepage.tsx
- Shared UI for segment and childrenlayout.tsx
- Loading UI (wraps page in Suspense)loading.tsx
- Error UI (wraps page in Error Boundary)error.tsx
- 404 UInot-found.tsx
- API endpoint (Route Handler)route.ts
- Re-rendered layout UItemplate.tsx
- Parallel route fallbackdefault.tsx
Basic Routing
Static Route:
app/ ├── page.tsx → / ├── about/ │ └── page.tsx → /about └── blog/ └── page.tsx → /blog
Dynamic Route:
// app/blog/[slug]/page.tsx export default function BlogPost({ params }: { params: { slug: string } }) { return <h1>Post: {params.slug}</h1> }
Catch-all Route:
// app/shop/[...slug]/page.tsx export default function Shop({ params }: { params: { slug: string[] } }) { return <h1>Category: {params.slug.join('/')}</h1> }
Optional Catch-all:
// app/docs/[[...slug]]/page.tsx // Matches /docs, /docs/a, /docs/a/b, etc.
Route Groups
Organize routes without affecting URL:
app/ ├── (marketing)/ # Group without URL segment │ ├── about/page.tsx → /about │ └── blog/page.tsx → /blog └── (shop)/ ├── products/page.tsx → /products └── cart/page.tsx → /cart
Parallel Routes
Render multiple pages in same layout:
app/ ├── @team/ # Slot │ └── page.tsx ├── @analytics/ # Slot │ └── page.tsx └── layout.tsx # Uses both slots
// app/layout.tsx export default function Layout({ children, team, analytics, }: { children: React.ReactNode team: React.ReactNode analytics: React.ReactNode }) { return ( <> {children} {team} {analytics} </> ) }
Intercepting Routes
Intercept routes to show in modal:
app/ ├── feed/ │ └── page.tsx ├── photo/ │ └── [id]/ │ └── page.tsx └── (..)photo/ # Intercepts /photo/[id] └── [id]/ └── page.tsx
Layouts
Root Layout (Required)
// app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) }
Nested Layouts
// app/dashboard/layout.tsx export default function DashboardLayout({ children, }: { children: React.ReactNode }) { return ( <section> <nav>Dashboard Nav</nav> {children} </section> ) }
Layouts are:
- Shared across multiple pages
- Preserve state on navigation
- Do not re-render on navigation
- Can fetch data
Server and Client Components
Server Components (Default)
Components in
app/ are Server Components by default:
// app/page.tsx (Server Component) async function getData() { const res = await fetch('https://api.example.com/data') return res.json() } export default async function Page() { const data = await getData() return <div>{data.title}</div> }
Benefits:
- Fetch data on server
- Access backend resources directly
- Keep sensitive data on server
- Reduce client-side JavaScript
- Improve initial page load
Limitations:
- Cannot use hooks (useState, useEffect)
- Cannot use browser APIs
- Cannot add event listeners
Client Components
Mark components with
'use client' directive:
// components/counter.tsx 'use client' import { useState } from 'react' export function Counter() { const [count, setCount] = useState(0) return ( <button onClick={() => setCount(count + 1)}> Count: {count} </button> ) }
Use Client Components for:
- Interactive UI (onClick, onChange)
- State management (useState, useReducer)
- Effects (useEffect, useLayoutEffect)
- Browser APIs (localStorage, navigator)
- Custom hooks
- React class components
Composition Pattern
// app/page.tsx (Server Component) import { ClientComponent } from './client-component' export default function Page() { return ( <div> <h1>Server-rendered content</h1> <ClientComponent /> </div> ) }
Data Fetching
Server Component Data Fetching
// app/posts/page.tsx async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } // Revalidate every hour }) if (!res.ok) throw new Error('Failed to fetch') return res.json() } export default async function PostsPage() { const posts = await getPosts() return ( <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> ) }
Caching Strategies
Force Cache (Default):
fetch('https://api.example.com/data', { cache: 'force-cache' })
No Store (Dynamic):
fetch('https://api.example.com/data', { cache: 'no-store' })
Revalidate:
fetch('https://api.example.com/data', { next: { revalidate: 3600 } // Seconds })
Tag-based Revalidation:
fetch('https://api.example.com/data', { next: { tags: ['posts'] } }) // Revalidate elsewhere: import { revalidateTag } from 'next/cache' revalidateTag('posts')
Parallel Data Fetching
async function getData() { const [posts, users] = await Promise.all([ fetch('https://api.example.com/posts').then(r => r.json()), fetch('https://api.example.com/users').then(r => r.json()), ]) return { posts, users } }
Sequential Data Fetching
async function getData() { const post = await fetch(`https://api.example.com/posts/${id}`).then(r => r.json()) const author = await fetch(`https://api.example.com/users/${post.authorId}`).then(r => r.json()) return { post, author } }
Route Handlers (API Routes)
Basic Route Handler
// app/api/hello/route.ts export async function GET(request: Request) { return Response.json({ message: 'Hello' }) } export async function POST(request: Request) { const body = await request.json() return Response.json({ received: body }) }
Dynamic Route Handler
// app/api/posts/[id]/route.ts export async function GET( request: Request, { params }: { params: { id: string } } ) { const post = await getPost(params.id) return Response.json(post) } export async function DELETE( request: Request, { params }: { params: { id: string } } ) { await deletePost(params.id) return new Response(null, { status: 204 }) }
Request Helpers
export async function GET(request: Request) { const { searchParams } = new URL(request.url) const id = searchParams.get('id') const cookies = request.headers.get('cookie') return Response.json({ id }) }
Response Types
// JSON return Response.json({ data: 'value' }) // Text return new Response('Hello', { headers: { 'Content-Type': 'text/plain' } }) // Redirect return Response.redirect('https://example.com') // Status codes return new Response('Not Found', { status: 404 })
Navigation
Link Component
import Link from 'next/link' export default function Page() { return ( <> <Link href="/about">About</Link> <Link href="/blog/post-1">Post 1</Link> <Link href={{ pathname: '/blog/[slug]', query: { slug: 'post-1' } }}> Post 1 (alternative) </Link> </> ) }
useRouter Hook (Client)
'use client' import { useRouter } from 'next/navigation' export function NavigateButton() { const router = useRouter() return ( <button onClick={() => router.push('/dashboard')}> Dashboard </button> ) }
Router Methods:
- Navigate to routerouter.push(href)
- Replace current historyrouter.replace(href)
- Refresh current routerouter.refresh()
- Navigate backrouter.back()
- Navigate forwardrouter.forward()
- Prefetch routerouter.prefetch(href)
Programmatic Navigation (Server)
import { redirect } from 'next/navigation' export default async function Page() { const session = await getSession() if (!session) { redirect('/login') } return <div>Protected content</div> }
Metadata & SEO
Static Metadata
// app/page.tsx import { Metadata } from 'next' export const metadata: Metadata = { title: 'My Page', description: 'Page description', keywords: ['nextjs', 'react'], openGraph: { title: 'My Page', description: 'Page description', images: ['/og-image.jpg'], }, twitter: { card: 'summary_large_image', title: 'My Page', description: 'Page description', images: ['/twitter-image.jpg'], }, } export default function Page() { return <div>Content</div> }
Dynamic Metadata
// app/blog/[slug]/page.tsx export async function generateMetadata({ params }): 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], }, } }
Metadata Files
,favicon.ico
,icon.png
- Faviconsapple-icon.png
,opengraph-image.png
- Social imagestwitter-image.png
- Robots filerobots.txt
- Sitemapsitemap.xml
Image Optimization
Image Component
import Image from 'next/image' export default function Page() { return ( <> {/* Local image */} <Image src="/profile.png" alt="Profile" width={500} height={500} /> {/* Remote image */} <Image src="https://example.com/image.jpg" alt="Remote" width={500} height={500} /> {/* Responsive fill */} <div style={{ position: 'relative', width: '100%', height: '400px' }}> <Image src="/hero.jpg" alt="Hero" fill style={{ objectFit: 'cover' }} /> </div> {/* Priority loading */} <Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority /> </> ) }
Image Props:
- Image path (local or URL)src
- Alt text (required)alt
,width
- Dimensions (required unless fill)height
- Fill parent containerfill
- Responsive sizessizes
- 1-100 (default 75)quality
- Preload imagepriority
- 'blur' | 'empty'placeholder
- Data URL for blurblurDataURL
Remote Image Configuration
// next.config.js module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com', pathname: '/images/**', }, ], }, }
Font Optimization
Google Fonts
// app/layout.tsx import { Inter, Roboto_Mono } from 'next/font/google' const inter = Inter({ subsets: ['latin'], display: 'swap', }) const robotoMono = Roboto_Mono({ subsets: ['latin'], display: 'swap', variable: '--font-roboto-mono', }) export default function RootLayout({ children }) { return ( <html lang="en" className={`${inter.className} ${robotoMono.variable}`}> <body>{children}</body> </html> ) }
Local Fonts
import localFont from 'next/font/local' const myFont = localFont({ src: './fonts/my-font.woff2', display: 'swap', variable: '--font-my-font', })
Loading States
Loading File
// app/dashboard/loading.tsx export default function Loading() { return <div>Loading dashboard...</div> }
Streaming with Suspense
// app/page.tsx import { Suspense } from 'react' async function Posts() { const posts = await getPosts() return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul> } export default function Page() { return ( <div> <h1>My Posts</h1> <Suspense fallback={<div>Loading posts...</div>}> <Posts /> </Suspense> </div> ) }
Error Handling
Error File
// app/error.tsx 'use client' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div> <h2>Something went wrong!</h2> <p>{error.message}</p> <button onClick={() => reset()}>Try again</button> </div> ) }
Global Error
// app/global-error.tsx '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> ) }
Not Found
// app/not-found.tsx export default function NotFound() { return ( <div> <h2>404 - Not Found</h2> <p>Could not find requested resource</p> </div> ) } // Trigger programmatically import { notFound } from 'next/navigation' export default async function Page({ params }) { const post = await getPost(params.id) if (!post) { notFound() } return <div>{post.title}</div> }
Middleware
// middleware.ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { // Authentication check const token = request.cookies.get('token') if (!token) { return NextResponse.redirect(new URL('/login', request.url)) } // Add custom header const response = NextResponse.next() response.headers.set('x-custom-header', 'value') return response } export const config = { matcher: ['/dashboard/:path*', '/api/:path*'], }
Environment Variables
# .env.local DATABASE_URL=postgresql://... NEXT_PUBLIC_API_URL=https://api.example.com
// Server-side only const dbUrl = process.env.DATABASE_URL // Client and server (NEXT_PUBLIC_ prefix) const apiUrl = process.env.NEXT_PUBLIC_API_URL
Configuration
next.config.js
/** @type {import('next').NextConfig} */ const nextConfig = { // React strict mode reactStrictMode: true, // Image domains images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com' }, ], }, // Redirects async redirects() { return [ { source: '/old-page', destination: '/new-page', permanent: true, }, ] }, // Rewrites async rewrites() { return [ { source: '/api/:path*', destination: 'https://api.example.com/:path*', }, ] }, // Headers async headers() { return [ { source: '/(.*)', headers: [ { key: 'X-Frame-Options', value: 'DENY' }, ], }, ] }, // Environment variables env: { CUSTOM_KEY: 'value', }, } module.exports = nextConfig
Best Practices
- Use Server Components: Default to Server Components, use Client Components only when needed
- Optimize Images: Always use
for automatic optimizationnext/image - Metadata: Set proper metadata for SEO
- Loading States: Provide loading UI with Suspense
- Error Handling: Implement error boundaries
- Route Handlers: Use for API endpoints instead of separate backend
- Caching: Leverage built-in caching strategies
- Layouts: Use nested layouts to share UI
- TypeScript: Enable TypeScript for type safety
- Performance: Use
for above-fold images, lazy load below-foldpriority
Common Patterns
Protected Routes
// app/dashboard/layout.tsx import { redirect } from 'next/navigation' import { getSession } from '@/lib/auth' export default async function DashboardLayout({ children }) { const session = await getSession() if (!session) { redirect('/login') } return <>{children}</> }
Data Mutations (Server Actions)
// app/actions.ts 'use server' import { revalidatePath } from 'next/cache' export async function createPost(formData: FormData) { const title = formData.get('title') await db.post.create({ data: { title } }) revalidatePath('/posts') } // app/posts/new/page.tsx import { createPost } from '@/app/actions' export default function NewPost() { return ( <form action={createPost}> <input name="title" type="text" required /> <button type="submit">Create</button> </form> ) }
Static Generation
// Generate static params for dynamic routes export async function generateStaticParams() { const posts = await getPosts() return posts.map(post => ({ slug: post.slug, })) } export default async function Post({ params }) { const post = await getPost(params.slug) return <article>{post.content}</article> }
Deployment
Vercel (Recommended)
# Install Vercel CLI npm i -g vercel # Deploy vercel
Self-Hosting
# Build npm run build # Start production server npm start
Requirements:
- Node.js 18.17 or later
in next.config.js (optional, reduces size)output: 'standalone'
Docker
FROM node:18-alpine AS base FROM base AS deps WORKDIR /app COPY package*.json ./ RUN npm ci FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner WORKDIR /app ENV NODE_ENV production COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static EXPOSE 3000 CMD ["node", "server.js"]
Troubleshooting
Common Issues
-
Hydration errors
- Ensure server and client render same content
- Check for browser-only code in Server Components
- Verify no conditional rendering based on browser APIs
-
Images not loading
- Add remote domains to
next.config.js - Check image paths (use leading
for public)/ - Verify width/height provided
- Add remote domains to
-
API route 404
- Check file is named
notroute.ts/jsindex.ts - Verify export named GET/POST not default export
- Ensure in
directoryapp/api/
- Check file is named
-
"use client" errors
- Add
to components using hooks'use client' - Import Client Components in Server Components, not vice versa
- Check event handlers have
'use client'
- Add
-
Metadata not updating
- Clear browser cache
- Check metadata export is named correctly
- Verify async generateMetadata returns Promise<Metadata>
Resources
- Documentation: https://nextjs.org/docs
- Learn Course: https://nextjs.org/learn
- Examples: https://github.com/vercel/next.js/tree/canary/examples
- Blog: https://nextjs.org/blog
- GitHub: https://github.com/vercel/next.js
Implementation Checklist
When building with Next.js:
- Create project with
create-next-app - Configure TypeScript and ESLint
- Set up root layout with metadata
- Implement routing structure
- Add loading and error states
- Configure image optimization
- Set up font optimization
- Implement data fetching patterns
- Add API routes as needed
- Configure environment variables
- Set up middleware if needed
- Optimize for production build
- Test in production mode
- Configure deployment platform
- Set up monitoring and analytics