The Only Cheatsheets

Comprehensive collection of your cheatsheets

View project on GitHub

Complete Next.js Cheat Sheet - Beginner to Advanced

Download PDF

Table of Contents

  1. Getting Started
  2. Project Structure
  3. Pages and Routing
  4. Components
  5. Styling
  6. Data Fetching
  7. API Routes
  8. State Management
  9. Authentication
  10. Performance Optimization
  11. Deployment
  12. Advanced Features
  13. Best Practices
  14. Troubleshooting

Getting Started

Installation & Setup

# Create new Next.js app
npx create-next-app@latest my-app
cd my-app

# With TypeScript
npx create-next-app@latest my-app --typescript

# With specific template
npx create-next-app@latest my-app --example with-tailwindcss

# Start development server
npm run dev
# or
yarn dev
# or
pnpm dev

Essential Dependencies

# Core dependencies (usually included)
npm install next react react-dom

# TypeScript support
npm install --save-dev typescript @types/react @types/node

# Styling
npm install tailwindcss postcss autoprefixer
npm install styled-components
npm install @emotion/react @emotion/styled

# State Management
npm install zustand
npm install @reduxjs/toolkit react-redux

# Forms
npm install react-hook-form
npm install formik yup

# HTTP Client
npm install axios
npm install swr

# Authentication
npm install next-auth
npm install @auth0/nextjs-auth0

# Database
npm install prisma @prisma/client
npm install mongoose

Project Structure

App Router Structure (Next.js 13+)

my-app/
├── app/
│   ├── globals.css
│   ├── layout.tsx
│   ├── page.tsx
│   ├── loading.tsx
│   ├── error.tsx
│   ├── not-found.tsx
│   ├── about/
│   │   └── page.tsx
│   ├── blog/
│   │   ├── page.tsx
│   │   └── [slug]/
│   │       └── page.tsx
│   └── api/
│       ├── auth/
│       └── users/
├── components/
├── lib/
├── public/
├── styles/
└── next.config.js

Pages Router Structure (Legacy)

my-app/
├── pages/
│   ├── _app.tsx
│   ├── _document.tsx
│   ├── index.tsx
│   ├── about.tsx
│   ├── blog/
│   │   ├── index.tsx
│   │   └── [slug].tsx
│   └── api/
│       ├── auth/
│       └── users.ts
├── components/
├── lib/
├── public/
├── styles/
└── next.config.js

Pages and Routing

App Router (Next.js 13+)

Basic Page Creation

// app/page.tsx (Home page)
export default function HomePage() {
  return <h1>Welcome to Next.js!</h1>
}

// app/about/page.tsx
export default function AboutPage() {
  return <h1>About Us</h1>
}

// app/blog/[slug]/page.tsx (Dynamic route)
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>Blog Post: {params.slug}</h1>
}

Layout Components

// app/layout.tsx (Root layout)
import './globals.css'

export const metadata = {
  title: 'My App',
  description: 'Generated by Next.js',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <nav>Navigation</nav>
        <main>{children}</main>
        <footer>Footer</footer>
      </body>
    </html>
  )
}

// app/blog/layout.tsx (Nested layout)
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="blog-container">
      <aside>Blog Sidebar</aside>
      <div>{children}</div>
    </div>
  )
}

Special Files

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

// 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/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
    </div>
  )
}

Pages Router (Legacy)

Basic Pages

// pages/index.tsx
export default function Home() {
  return <h1>Home Page</h1>
}

// pages/about.tsx
export default function About() {
  return <h1>About Page</h1>
}

// pages/blog/[slug].tsx
import { useRouter } from 'next/router'

export default function BlogPost() {
  const router = useRouter()
  const { slug } = router.query

  return <h1>Blog Post: {slug}</h1>
}

Custom App and Document

// pages/_app.tsx
import type { AppProps } from 'next/app'
import '../styles/globals.css'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <div>
      <nav>Navigation</nav>
      <Component {...pageProps} />
    </div>
  )
}

// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}
import Link from 'next/link'

// Basic link
<Link href="/about">About</Link>

// Dynamic link
<Link href={`/blog/${post.slug}`}>Read More</Link>

// External link
<Link href="https://example.com" target="_blank">
  External Link
</Link>

// With custom styling
<Link href="/about" className="nav-link">
  About
</Link>

Programmatic Navigation

// App Router
import { useRouter } from 'next/navigation'

function MyComponent() {
  const router = useRouter()
  
  const handleClick = () => {
    router.push('/about')
    router.replace('/about') // No history entry
    router.back()
    router.forward()
    router.refresh()
  }
}

// Pages Router
import { useRouter } from 'next/router'

function MyComponent() {
  const router = useRouter()
  
  const handleClick = () => {
    router.push('/about')
    router.replace('/about')
    router.back()
    router.reload()
  }
}

Components

Client vs Server Components (App Router)

Server Components (Default)

// app/components/ServerComponent.tsx
// Server components run on the server
async function ServerComponent() {
  const data = await fetch('https://api.example.com/data')
  const json = await data.json()
  
  return (
    <div>
      <h1>Server Component</h1>
      <pre>{JSON.stringify(json, null, 2)}</pre>
    </div>
  )
}

Client Components

// app/components/ClientComponent.tsx
'use client'

import { useState, useEffect } from 'react'

export default function ClientComponent() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    console.log('Client component mounted')
  }, [])
  
  return (
    <div>
      <h1>Client Component</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  )
}

Component Patterns

Higher-Order Component (HOC)

function withAuth<T extends object>(Component: React.ComponentType<T>) {
  return function AuthenticatedComponent(props: T) {
    const [isAuthenticated, setIsAuthenticated] = useState(false)
    
    if (!isAuthenticated) {
      return <div>Please log in</div>
    }
    
    return <Component {...props} />
  }
}

// Usage
const ProtectedPage = withAuth(MyPage)

Custom Hooks

// hooks/useCounter.ts
import { useState } from 'react'

export function useCounter(initialValue: number = 0) {
  const [count, setCount] = useState(initialValue)
  
  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)
  const reset = () => setCount(initialValue)
  
  return { count, increment, decrement, reset }
}

// Usage in component
function Counter() {
  const { count, increment, decrement, reset } = useCounter(0)
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

Styling

CSS Modules

// styles/Home.module.css
.container {
  padding: 2rem;
  background-color: #f0f0f0;
}

.title {
  font-size: 2rem;
  color: #333;
}

// components/Home.tsx
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      <h1 className={styles.title}>Home Page</h1>
    </div>
  )
}

Tailwind CSS

// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
    './app/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#64748b',
      },
    },
  },
  plugins: [],
}

// Component usage
export default function Button({ children, variant = 'primary' }) {
  const baseClasses = 'px-4 py-2 rounded font-medium'
  const variantClasses = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-500 text-white hover:bg-gray-600',
  }
  
  return (
    <button className={`${baseClasses} ${variantClasses[variant]}`}>
      {children}
    </button>
  )
}

Styled Components

// components/StyledButton.tsx
import styled from 'styled-components'

const StyledButton = styled.button`
  background-color: ${props => props.primary ? '#3b82f6' : '#64748b'};
  color: white;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 0.25rem;
  cursor: pointer;
  
  &:hover {
    opacity: 0.8;
  }
`

export default function Button({ primary, children }) {
  return (
    <StyledButton primary={primary}>
      {children}
    </StyledButton>
  )
}

Global Styles

/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

html,
body {
  max-width: 100vw;
  overflow-x: hidden;
  font-family: Inter, sans-serif;
}

@layer components {
  .btn-primary {
    @apply bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded;
  }
}

Data Fetching

App Router Data Fetching

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    cache: 'force-cache', // Default caching
  })
  
  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }
  
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()
  
  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  )
}

Caching Options

// No caching
fetch('https://api.example.com/data', { cache: 'no-store' })

// Revalidate every 60 seconds
fetch('https://api.example.com/data', { next: { revalidate: 60 } })

// Cache forever (default)
fetch('https://api.example.com/data', { cache: 'force-cache' })

Dynamic Data with Client Components

'use client'

import { useState, useEffect } from 'react'

export default function ClientPosts() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    async function fetchPosts() {
      try {
        const res = await fetch('/api/posts')
        const data = await res.json()
        setPosts(data)
      } catch (error) {
        console.error('Error fetching posts:', error)
      } finally {
        setLoading(false)
      }
    }
    
    fetchPosts()
  }, [])
  
  if (loading) return <div>Loading...</div>
  
  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}

Pages Router Data Fetching

getStaticProps (SSG)

// pages/posts.tsx
export async function getStaticProps() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts')
  const posts = await res.json()
  
  return {
    props: {
      posts,
    },
    revalidate: 60, // ISR - revalidate every 60 seconds
  }
}

export default function Posts({ posts }) {
  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}

getServerSideProps (SSR)

// pages/profile.tsx
export async function getServerSideProps(context) {
  const { req, res, params, query } = context
  
  // Access cookies, headers, etc.
  const token = req.cookies.token
  
  if (!token) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }
  
  const userData = await fetchUserData(token)
  
  return {
    props: {
      user: userData,
    },
  }
}

getStaticPaths (Dynamic SSG)

// pages/posts/[id].tsx
export async function getStaticPaths() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts')
  const posts = await res.json()
  
  const paths = posts.map(post => ({
    params: { id: post.id.toString() },
  }))
  
  return {
    paths,
    fallback: 'blocking', // or false, true
  }
}

export async function getStaticProps({ params }) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`)
  const post = await res.json()
  
  return {
    props: {
      post,
    },
  }
}

SWR for Client-Side Data Fetching

import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(res => res.json())

export default function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)
  
  if (error) return <div>Failed to load</div>
  if (isLoading) return <div>Loading...</div>
  
  return <div>Hello {data.name}!</div>
}

// With custom hook
function useUser(id: string) {
  const { data, error, isLoading } = useSWR(
    id ? `/api/user/${id}` : null,
    fetcher
  )
  
  return {
    user: data,
    isLoading,
    isError: error
  }
}

API Routes

App Router API Routes

Basic API Route

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const query = searchParams.get('query')
  
  // Fetch data from database
  const users = await fetchUsers(query)
  
  return NextResponse.json({ users })
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  
  // Validate data
  if (!body.name || !body.email) {
    return NextResponse.json(
      { error: 'Name and email are required' },
      { status: 400 }
    )
  }
  
  // Create user
  const user = await createUser(body)
  
  return NextResponse.json({ user }, { status: 201 })
}

Dynamic API Routes

// app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const id = params.id
  const user = await fetchUserById(id)
  
  if (!user) {
    return NextResponse.json(
      { error: 'User not found' },
      { status: 404 }
    )
  }
  
  return NextResponse.json({ user })
}

export async function PUT(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const id = params.id
  const body = await request.json()
  
  const updatedUser = await updateUser(id, body)
  
  return NextResponse.json({ user: updatedUser })
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const id = params.id
  await deleteUser(id)
  
  return NextResponse.json({ message: 'User deleted' })
}

Pages Router API Routes

Basic API Route

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { method } = req
  
  switch (method) {
    case 'GET':
      const users = await fetchUsers()
      res.status(200).json({ users })
      break
      
    case 'POST':
      const { name, email } = req.body
      
      if (!name || !email) {
        return res.status(400).json({ error: 'Name and email required' })
      }
      
      const user = await createUser({ name, email })
      res.status(201).json({ user })
      break
      
    default:
      res.setHeader('Allow', ['GET', 'POST'])
      res.status(405).end(`Method ${method} Not Allowed`)
  }
}

Dynamic API Route

// pages/api/users/[id].ts
export default async function handler(req, res) {
  const { id } = req.query
  const { method } = req
  
  switch (method) {
    case 'GET':
      const user = await fetchUserById(id)
      if (!user) {
        return res.status(404).json({ error: 'User not found' })
      }
      res.status(200).json({ user })
      break
      
    case 'PUT':
      const updatedUser = await updateUser(id, req.body)
      res.status(200).json({ user: updatedUser })
      break
      
    case 'DELETE':
      await deleteUser(id)
      res.status(200).json({ message: 'User deleted' })
      break
      
    default:
      res.setHeader('Allow', ['GET', 'PUT', 'DELETE'])
      res.status(405).end(`Method ${method} Not Allowed`)
  }
}

Middleware

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Check authentication
  const token = request.cookies.get('token')
  
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  // Add custom headers
  const response = NextResponse.next()
  response.headers.set('X-Custom-Header', 'custom-value')
  
  return response
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/:path*',
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

State Management

React Built-in State

// useState
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

// useReducer
import { useReducer } from 'react'

const initialState = { count: 0 }

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return initialState
    default:
      throw new Error()
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  )
}

Context API

// contexts/AuthContext.tsx
import { createContext, useContext, useState } from 'react'

type AuthContextType = {
  user: User | null
  login: (user: User) => void
  logout: () => void
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  
  const login = (user: User) => {
    setUser(user)
    localStorage.setItem('user', JSON.stringify(user))
  }
  
  const logout = () => {
    setUser(null)
    localStorage.removeItem('user')
  }
  
  return (
    <AuthContext.Provider value=>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

// Usage in component
function Profile() {
  const { user, logout } = useAuth()
  
  return (
    <div>
      <h1>Welcome, {user?.name}</h1>
      <button onClick={logout}>Logout</button>
    </div>
  )
}

Zustand

// store/useStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

export const useCounterStore = create<CounterState>()(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    {
      name: 'counter-storage',
    }
  )
)

// Usage in component
function Counter() {
  const { count, increment, decrement, reset } = useCounterStore()
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

Redux Toolkit

// store/store.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

// store/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

// Usage in component
import { useSelector, useDispatch } from 'react-redux'
import { RootState } from '../store/store'
import { increment, decrement } from '../store/counterSlice'

function Counter() {
  const count = useSelector((state: RootState) => state.counter.value)
  const dispatch = useDispatch()
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}

Authentication

NextAuth.js Setup

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import CredentialsProvider from 'next-auth/providers/credentials'

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        // Validate credentials
        const user = await validateUser(credentials)
        
        if (user) {
          return {
            id: user.id,
            email: user.email,
            name: user.name,
          }
        }
        return null
      }
    })
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
      }
      return token
    },
    async session({ session, token }) {
      session.user.id = token.id
      return session
    },
  },
  pages: {
    signIn: '/auth/signin',
    signOut: '/auth/signout',
    error: '/auth/error',
  },
})

export { handler as GET, handler as POST }

Session Provider Setup

// app/providers.tsx
'use client'

import { SessionProvider } from 'next-auth/react'

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>
}

// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Using Authentication in Components

'use client'

import { useSession, signIn, signOut } from 'next-auth/react'

export default function Component() {
  const { data: session, status } = useSession()
  
  if (status === 'loading') return <p>Loading...</p>
  
  if (session) {
    return (
      <>
        <p>Signed in as {session.user?.email}</p>
        <button onClick={() => signOut()}>Sign out</button>
      </>
    )
  }
  
  return (
    <>
      <p>Not signed in</p>
      <button onClick={() => signIn()}>Sign in</button>
    </>
  )
}

Server-Side Authentication

// app/protected/page.tsx
import { getServerSession } from 'next-auth/next'
import { redirect } from 'next/navigation'

export default async function ProtectedPage() {
  const session = await getServerSession()
  
  if (!session) {
    redirect('/auth/signin')
  }
  
  return (
    <div>
      <h1>Protected Content</h1>
      <p>Welcome, {session.user?.name}!</p>
    </div>
  )
}

Custom Login Page

// app/auth/signin/page.tsx
'use client'

import { signIn, getProviders } from 'next-auth/react'
import { useState, useEffect } from 'react'

export default function SignIn() {
  const [providers, setProviders] = useState(null)
  
  useEffect(() => {
    const fetchProviders = async () => {
      const res = await getProviders()
      setProviders(res)
    }
    fetchProviders()
  }, [])
  
  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="max-w-md w-full space-y-8">
        <h2 className="text-center text-3xl font-bold">Sign in</h2>
        
        {providers && Object.values(providers).map((provider) => (
          <div key={provider.name}>
            <button
              onClick={() => signIn(provider.id)}
              className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
            >
              Sign in with {provider.name}
            </button>
          </div>
        ))}
      </div>
    </div>
  )
}

Performance Optimization

Image Optimization

import Image from 'next/image'

// Basic usage
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={800}
  height={600}
/>

// With priority loading
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={800}
  height={600}
  priority
/>

// Fill container
<div style=>
  <Image
    src="/hero.jpg"
    alt="Hero image"
    fill
    style=
  />
</div>

// Responsive images
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

Font Optimization

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Code Splitting

// Dynamic imports
import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(() => import('../components/MyComponent'), {
  loading: () => <p>Loading...</p>,
  ssr: false, // Disable server-side rendering
})

// Conditional loading
const DynamicChart = dynamic(() => import('../components/Chart'), {
  loading: () => <p>Loading chart...</p>,
})

function Dashboard() {
  const [showChart, setShowChart] = useState(false)
  
  return (
    <div>
      <h1>Dashboard</h1>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      {showChart && <DynamicChart />}
    </div>
  )
}

Memoization

import { memo, useMemo, useCallback } from 'react'

// Component memoization
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  )
})

// Value memoization
function Component({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter(item => item.category === filter)
  }, [items, filter])
  
  const handleClick = useCallback((id) => {
    console.log('Item clicked:', id)
  }, [])
  
  return (
    <div>
      {filteredItems.map(item => (
        <div key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </div>
      ))}
    </div>
  )
}

Bundle Analysis

# Install bundle analyzer
npm install --save-dev @next/bundle-analyzer

# Add to next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // Your Next.js config
})

# Run analysis
ANALYZE=true npm run build

Deployment

Vercel Deployment

# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

# Environment variables
vercel env add NEXT_PUBLIC_API_URL
vercel env add DATABASE_URL

Docker Deployment

# Dockerfile
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]

Environment Variables

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://username:password@localhost:5432/mydb
NEXTAUTH_SECRET=your-secret-key
NEXTAUTH_URL=http://localhost:3000

Next.js Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
  images: {
    domains: ['example.com', 'cdn.example.com'],
  },
  env: {
    CUSTOM_KEY: 'custom-value',
  },
  async redirects() {
    return [
      {
        source: '/old-path',
        destination: '/new-path',
        permanent: true,
      },
    ]
  },
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://api.example.com/:path*',
      },
    ]
  },
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Custom-Header',
            value: 'custom-value',
          },
        ],
      },
    ]
  },
}

module.exports = nextConfig

Advanced Features

Internationalization (i18n)

// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'fr', 'es'],
    defaultLocale: 'en',
    domains: [
      {
        domain: 'example.com',
        defaultLocale: 'en',
      },
      {
        domain: 'example.fr',
        defaultLocale: 'fr',
      },
    ],
  },
}
// pages/index.tsx
import { useRouter } from 'next/router'
import { GetStaticProps } from 'next'

export default function Home({ translations }) {
  const { locale, locales, asPath } = useRouter()
  
  return (
    <div>
      <h1>{translations.title}</h1>
      <p>Current locale: {locale}</p>
      <p>Available locales: {locales.join(', ')}</p>
    </div>
  )
}

export const getStaticProps: GetStaticProps = async ({ locale }) => {
  const translations = await import(`../locales/${locale}.json`)
  
  return {
    props: {
      translations: translations.default,
    },
  }
}

Progressive Web App (PWA)

// next.config.js
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
})

module.exports = withPWA({
  // Your Next.js config
})
// public/manifest.json
{
  "name": "My Next.js App",
  "short_name": "NextApp",
  "description": "A Next.js PWA",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Streaming and Suspense

// App Router with Streaming
import { Suspense } from 'react'

async function SlowComponent() {
  await new Promise(resolve => setTimeout(resolve, 2000))
  return <div>Slow content loaded!</div>
}

export default function Page() {
  return (
    <div>
      <h1>Fast content</h1>
      <Suspense fallback={<div>Loading slow content...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

Edge Runtime

// app/api/edge/route.ts
export const runtime = 'edge'

export async function GET() {
  return new Response('Hello from Edge Runtime!')
}

Server Actions (App Router)

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  
  // Save to database
  await savePost({ title, content })
  
  // Revalidate the posts page
  revalidatePath('/posts')
}

// app/create-post/page.tsx
import { createPost } from '../actions'

export default function CreatePost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Create Post</button>
    </form>
  )
}

Best Practices

File and Folder Structure

src/
├── app/
│   ├── (auth)/
│   │   ├── login/
│   │   └── register/
│   ├── dashboard/
│   │   ├── analytics/
│   │   └── settings/
│   └── globals.css
├── components/
│   ├── ui/
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   └── Modal.tsx
│   ├── forms/
│   └── layout/
├── lib/
│   ├── auth.ts
│   ├── db.ts
│   └── utils.ts
├── hooks/
│   ├── useAuth.ts
│   └── useLocalStorage.ts
├── types/
│   └── index.ts
└── utils/
    ├── constants.ts
    └── helpers.ts

Error Handling

// Global error boundary
'use client'

import { useEffect } from 'react'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])

  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

// Custom error page for specific routes
// app/dashboard/error.tsx
'use client'

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="error-container">
      <h2>Dashboard Error</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  )
}

SEO Optimization

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPost(params.slug)
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  }
}

// JSON-LD structured data
export default function BlogPost({ params }) {
  const post = await getPost(params.slug)
  
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    author: {
      '@type': 'Person',
      name: post.author.name,
    },
    datePublished: post.publishedAt,
  }
  
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML=
      />
      <article>
        <h1>{post.title}</h1>
        <p>{post.content}</p>
      </article>
    </>
  )
}

Testing

// __tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import Button from '../components/Button'

describe('Button', () => {
  it('renders button with text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })
  
  it('calls onClick handler when clicked', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

// __tests__/pages/api/users.test.ts
import { createMocks } from 'node-mocks-http'
import handler from '../pages/api/users'

describe('/api/users', () => {
  it('returns users for GET request', async () => {
    const { req, res } = createMocks({
      method: 'GET',
    })
    
    await handler(req, res)
    
    expect(res._getStatusCode()).toBe(200)
    const data = JSON.parse(res._getData())
    expect(data).toHaveProperty('users')
  })
})

Security Best Practices

// Content Security Policy
// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
]

module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ]
  },
}

// Input validation and sanitization
import { z } from 'zod'

const userSchema = z.object({
  name: z.string().min(1).max(50),
  email: z.string().email(),
  age: z.number().min(18).max(120),
})

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const validatedData = userSchema.parse(body)
    
    // Process validated data
    const user = await createUser(validatedData)
    
    return NextResponse.json({ user })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Invalid input', details: error.errors },
        { status: 400 }
      )
    }
    
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

Troubleshooting

Common Issues and Solutions

Hydration Errors

// Problem: Hydration mismatch
function MyComponent() {
  const [mounted, setMounted] = useState(false)
  
  useEffect(() => {
    setMounted(true)
  }, [])
  
  if (!mounted) {
    return null // or loading skeleton
  }
  
  return <div>{new Date().toLocaleDateString()}</div>
}

// Alternative: Use dynamic import with ssr: false
const DynamicComponent = dynamic(() => import('./MyComponent'), {
  ssr: false,
})

CORS Issues

// pages/api/cors-example.ts
import { NextApiRequest, NextApiResponse } from 'next'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // Set CORS headers
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT')
  res.setHeader('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version')
  
  if (req.method === 'OPTIONS') {
    res.status(200).end()
    return
  }
  
  // Handle other methods
  res.status(200).json({ message: 'Hello World' })
}

Memory Leaks

// Cleanup useEffect
useEffect(() => {
  const timer = setInterval(() => {
    console.log('Timer tick')
  }, 1000)
  
  return () => {
    clearInterval(timer) // Cleanup
  }
}, [])

// Cleanup event listeners
useEffect(() => {
  const handleResize = () => {
    console.log('Window resized')
  }
  
  window.addEventListener('resize', handleResize)
  
  return () => {
    window.removeEventListener('resize', handleResize)
  }
}, [])

Performance Issues

# Check bundle size
npm run build

# Analyze bundle
npm install --save-dev webpack-bundle-analyzer
ANALYZE=true npm run build

# Check core web vitals
npm install --save-dev @next/bundle-analyzer

Debugging Tools

// Custom debug component
function DebugInfo({ data }) {
  if (process.env.NODE_ENV !== 'development') {
    return null
  }
  
  return (
    <div style=>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

// Usage
function MyComponent() {
  const [state, setState] = useState({ count: 0 })
  
  return (
    <div>
      <button onClick={() => setState({ count: state.count + 1 })}>
        Count: {state.count}
      </button>
      <DebugInfo data={state} />
    </div>
  )
}

Useful Resources

Official Documentation

Community Resources

Tools and Libraries

This cheat sheet covers the essential concepts and patterns you’ll need for Next.js development, from basic setup to advanced features. Keep it handy as a reference while building your applications!