Next.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 app/ directory
-
Layouts, loading states, and error boundaries
-
Streaming and Suspense support
-
Nested routing with layouts
Pages Router (Legacy):
-
Traditional page-based routing in pages/ directory
-
Uses getStaticProps , getServerSideProps , getInitialProps
-
Still supported for existing projects
Key Architectural Principles
-
Server Components by Default: Components in app/ are Server Components unless marked with '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)
-
src/ directory? (Optional)
-
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.tsx
-
Page UI for route
-
layout.tsx
-
Shared UI for segment and children
-
loading.tsx
-
Loading UI (wraps page in Suspense)
-
error.tsx
-
Error UI (wraps page in Error Boundary)
-
not-found.tsx
-
404 UI
-
route.ts
-
API endpoint (Route Handler)
-
template.tsx
-
Re-rendered layout UI
-
default.tsx
-
Parallel route fallback
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:
-
router.push(href)
-
Navigate to route
-
router.replace(href)
-
Replace current history
-
router.refresh()
-
Refresh current route
-
router.back()
-
Navigate back
-
router.forward()
-
Navigate forward
-
router.prefetch(href)
-
Prefetch route
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 , apple-icon.png
-
Favicons
-
opengraph-image.png , twitter-image.png
-
Social images
-
robots.txt
-
Robots file
-
sitemap.xml
-
Sitemap
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:
-
src
-
Image path (local or URL)
-
alt
-
Alt text (required)
-
width , height
-
Dimensions (required unless fill)
-
fill
-
Fill parent container
-
sizes
-
Responsive sizes
-
quality
-
1-100 (default 75)
-
priority
-
Preload image
-
placeholder
-
'blur-sm' | 'empty'
-
blurDataURL
-
Data URL for blur
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 next/image for automatic optimization
-
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 priority for above-fold images, lazy load below-fold
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
-
output: 'standalone' in next.config.js (optional, reduces size)
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
API route 404
-
Check file is named route.ts/js not index.ts
-
Verify export named GET/POST not default export
-
Ensure in app/api/ directory
"use client" errors
-
Add 'use client' to components using hooks
-
Import Client Components in Server Components, not vice versa
-
Check event handlers have 'use client'
Metadata not updating
-
Clear browser cache
-
Check metadata export is named correctly
-
Verify async generateMetadata returns Promise
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
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