Next.js Cheat Sheet

Modern Next.js 14+ with App Router, Server Components, and performance optimizations

App Router

Next.js App Router structure, routing, and layout patterns

File-based Routing Structure

App Router directory structure and file conventions

Beginnerroutingapp-routerbasicsstructure
app/
├── layout.tsx          # Root layout (required)
├── page.tsx           # Home page (/)
├── loading.tsx        # Loading UI
├── error.tsx          # Error UI
├── not-found.tsx      # 404 page
├── global-error.tsx   # Global error boundary
├── blog/
│   ├── layout.tsx     # Blog layout
│   ├── page.tsx       # Blog home (/blog)
│   ├── loading.tsx    # Blog loading UI
│   └── [slug]/
│       ├── page.tsx   # Blog post (/blog/[slug])
│       └── loading.tsx
├── dashboard/
│   ├── layout.tsx
│   ├── page.tsx       # /dashboard
│   ├── settings/
│   │   └── page.tsx   # /dashboard/settings
│   └── users/
│       ├── page.tsx   # /dashboard/users
│       └── [id]/
│           └── page.tsx # /dashboard/users/[id]
└── api/
    ├── users/
    │   └── route.ts   # API: /api/users
    └── auth/
        └── route.ts   # API: /api/auth

Root Layout

Creating the root layout with metadata and providers

Intermediatelayoutmetadataserver-components
// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: {
    default: 'My App',
    template: '%s | My App',
  },
  description: 'A modern Next.js application',
  keywords: ['Next.js', 'React', 'TypeScript'],
  authors: [{ name: 'Your Name' }],
  creator: 'Your Name',
};

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

Dynamic Routes and Params

Creating dynamic routes with parameters and catch-all segments

// app/blog/[slug]/page.tsx
interface PageProps {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function BlogPost({ params, searchParams }: PageProps) {
  const { slug } = await params;
  const search = await searchParams;

  // Fetch post data
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

// Generate static params for dynamic routes
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then((res) =>
    res.json()
  );

  return posts.map((post: any) => ({
    slug: post.slug,
  }));
}

// Generate metadata for each page
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  };
}

// Catch-all routes: [...slug] or [[...slug]]
// app/docs/[[...slug]]/page.tsx
interface DocsPageProps {
  params: Promise<{ slug?: string[] }>;
}

export default async function DocsPage({ params }: DocsPageProps) {
  const { slug = [] } = await params;
  const path = slug.join('/');

  return <div>Docs path: {path}</div>;
}

Parallel and Intercepted Routes

Advanced routing patterns with parallel and intercepted routes

// Parallel Routes Structure
app/
├── layout.tsx
├── page.tsx
├── @analytics/
│   ├── page.tsx       # Default analytics
│   └── loading.tsx
├── @team/
│   ├── page.tsx       # Default team
│   └── error.tsx
└── dashboard/
    ├── page.tsx
    ├── @analytics/
    │   └── page.tsx   # Analytics for dashboard
    └── @team/
        └── page.tsx   # Team for dashboard

// app/layout.tsx - Using parallel routes
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div>
      <div>{children}</div>
      <div className="grid grid-cols-2 gap-4">
        <div>{analytics}</div>
        <div>{team}</div>
      </div>
    </div>
  );
}

// Intercepted Routes Structure
app/
├── layout.tsx
├── page.tsx
├── photo/
│   └── [id]/
│       └── page.tsx   # Photo detail page
└── @modal/
    ├── default.tsx    # Default slot content
    └── (..)photo/     # Intercept /photo/[id]
        └── [id]/
            └── page.tsx # Modal version

// app/@modal/(..)photo/[id]/page.tsx
import { Modal } from '@/components/modal';

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const photo = await getPhoto(id);

  return (
    <Modal>
      <img src={photo.url} alt={photo.title} />
    </Modal>
  );
}

Data Fetching

Server Components, data fetching strategies, and caching

Server Component Data Fetching

Fetching data in Server Components with different caching strategies

// Basic data fetching in Server Component
export default async function BlogPage() {
  // Static data (cached by default)
  const posts = await fetch('https://api.example.com/posts', {
    cache: 'force-cache' // Default behavior
  }).then(res => res.json());

  // Dynamic data (not cached)
  const recentPosts = await fetch('https://api.example.com/posts/recent', {
    cache: 'no-store'
  }).then(res => res.json());

  // Revalidated data (cached with time-based revalidation)
  const featuredPosts = await fetch('https://api.example.com/posts/featured', {
    next: { revalidate: 3600 } // Revalidate every hour
  }).then(res => res.json());

  // Tagged data (for on-demand revalidation)
  const categories = await fetch('https://api.example.com/categories', {
    next: { tags: ['categories'] }
  }).then(res => res.json());

  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

// Parallel data fetching
export default async function Dashboard() {
  // These requests will run in parallel
  const [users, posts, analytics] = await Promise.all([
    fetch('https://api.example.com/users').then(res => res.json()),
    fetch('https://api.example.com/posts').then(res => res.json()),
    fetch('https://api.example.com/analytics').then(res => res.json()),
  ]);

  return (
    <div>
      <UserStats users={users} />
      <PostsList posts={posts} />
      <AnalyticsDashboard data={analytics} />
    </div>
  );
}

Database and ORM Integration

Direct database queries in Server Components

// Database queries in Server Components
import { db } from '@/lib/db';
import { posts, users } from '@/lib/schema';
import { eq, desc, count } from 'drizzle-orm';

export default async function PostsPage() {
  // Direct database query
  const recentPosts = await db
    .select({
      id: posts.id,
      title: posts.title,
      excerpt: posts.excerpt,
      createdAt: posts.createdAt,
      author: users.name,
    })
    .from(posts)
    .leftJoin(users, eq(posts.authorId, users.id))
    .orderBy(desc(posts.createdAt))
    .limit(10);

  // Aggregate queries
  const postCount = await db
    .select({ count: count() })
    .from(posts);

  return (
    <div>
      <h1>Recent Posts ({postCount[0].count} total)</h1>
      {recentPosts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <small>By {post.author} on {post.createdAt.toLocaleDateString()}</small>
        </article>
      ))}
    </div>
  );
}

// Using Prisma
import { prisma } from '@/lib/prisma';

export default async function UsersPage() {
  const users = await prisma.user.findMany({
    include: {
      posts: {
        select: {
          id: true,
          title: true,
        },
      },
      _count: {
        select: {
          posts: true,
        },
      },
    },
  });

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user._count.posts} posts</p>
        </div>
      ))}
    </div>
  );
}

Client-Side Data Fetching

Data fetching in Client Components with SWR and React Query

'use client';

import useSWR from 'swr';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// SWR data fetching
const fetcher = (url: string) => fetch(url).then((res) => res.json());

export function UserProfile({ userId }: { userId: string }) {
  const { data: user, error, isLoading } = useSWR(
    `/api/users/${userId}`,
    fetcher,
    {
      refreshInterval: 30000, // Refresh every 30 seconds
      revalidateOnFocus: false,
    }
  );

  if (error) return <div>Failed to load user</div>;
  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// React Query data fetching
export function PostsList() {
  const queryClient = useQueryClient();

  const {
    data: posts,
    isLoading,
    error,
  } = useQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then((res) => res.json()),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

  const createPostMutation = useMutation({
    mutationFn: (newPost: { title: string; content: string }) =>
      fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost),
      }).then((res) => res.json()),
    onSuccess: () => {
      // Invalidate and refetch posts
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  const handleCreatePost = (postData: { title: string; content: string }) => {
    createPostMutation.mutate(postData);
  };

  if (isLoading) return <div>Loading posts...</div>;
  if (error) return <div>Error loading posts</div>;

  return (
    <div>
      {posts?.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
      <button
        onClick={() => handleCreatePost({ title: 'New Post', content: 'Content' })}
        disabled={createPostMutation.isPending}
      >
        {createPostMutation.isPending ? 'Creating...' : 'Create Post'}
      </button>
    </div>
  );
}

// Streaming with Suspense
import { Suspense } from 'react';

export default function PostsPage() {
  return (
    <div>
      <h1>Posts</h1>
      <Suspense fallback={<PostsSkeleton />}>
        <PostsList />
      </Suspense>
      <Suspense fallback={<CommentsSkeleton />}>
        <CommentsList />
      </Suspense>
    </div>
  );
}

API Routes

Creating API endpoints with Route Handlers and middleware

Basic Route Handlers

Creating GET, POST, PUT, DELETE API endpoints

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { users } from '@/lib/schema';

// GET /api/users
export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const page = parseInt(searchParams.get('page') || '1');
    const limit = parseInt(searchParams.get('limit') || '10');
    const offset = (page - 1) * limit;

    const allUsers = await db
      .select()
      .from(users)
      .limit(limit)
      .offset(offset);

    return NextResponse.json({
      users: allUsers,
      pagination: {
        page,
        limit,
        hasMore: allUsers.length === limit,
      },
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch users' },
      { status: 500 }
    );
  }
}

// POST /api/users
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { name, email } = body;

    // Validation
    if (!name || !email) {
      return NextResponse.json(
        { error: 'Name and email are required' },
        { status: 400 }
      );
    }

    const [newUser] = await db
      .insert(users)
      .values({ name, email })
      .returning();

    return NextResponse.json(newUser, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create user' },
      { status: 500 }
    );
  }
}

Dynamic Route Parameters

Handling dynamic parameters in API routes

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

interface RouteParams {
  params: Promise<{ id: string }>;
}

// GET /api/users/[id]
export async function GET(
  request: NextRequest,
  { params }: RouteParams
) {
  try {
    const { id } = await params;

    const user = await db
      .select()
      .from(users)
      .where(eq(users.id, parseInt(id)))
      .limit(1);

    if (user.length === 0) {
      return NextResponse.json(
        { error: 'User not found' },
        { status: 404 }
      );
    }

    return NextResponse.json(user[0]);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch user' },
      { status: 500 }
    );
  }
}

// PUT /api/users/[id]
export async function PUT(
  request: NextRequest,
  { params }: RouteParams
) {
  try {
    const { id } = await params;
    const body = await request.json();
    const { name, email } = body;

    const [updatedUser] = await db
      .update(users)
      .set({ name, email, updatedAt: new Date() })
      .where(eq(users.id, parseInt(id)))
      .returning();

    if (!updatedUser) {
      return NextResponse.json(
        { error: 'User not found' },
        { status: 404 }
      );
    }

    return NextResponse.json(updatedUser);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to update user' },
      { status: 500 }
    );
  }
}

// DELETE /api/users/[id]
export async function DELETE(
  request: NextRequest,
  { params }: RouteParams
) {
  try {
    const { id } = await params;

    const [deletedUser] = await db
      .delete(users)
      .where(eq(users.id, parseInt(id)))
      .returning();

    if (!deletedUser) {
      return NextResponse.json(
        { error: 'User not found' },
        { status: 404 }
      );
    }

    return NextResponse.json({ message: 'User deleted successfully' });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to delete user' },
      { status: 500 }
    );
  }
}

Styling & Components

CSS, Tailwind, and component patterns in Next.js

Global Styles and CSS Modules

Setting up global styles and component-scoped CSS

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

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(
      to bottom,
      transparent,
      rgb(var(--background-end-rgb))
    )
    rgb(var(--background-start-rgb));
}

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

  .card {
    @apply bg-white shadow-md rounded-lg p-6 border border-gray-200;
  }
}

/* components/Button.module.css - CSS Modules */
.button {
  padding: 12px 24px;
  border-radius: 8px;
  border: none;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}

.primary {
  background-color: #3b82f6;
  color: white;
}

.primary:hover {
  background-color: #2563eb;
  transform: translateY(-1px);
}

.secondary {
  background-color: #6b7280;
  color: white;
}

.disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Server and Client Components

Creating reusable Server and Client Components

// components/ServerComponent.tsx - Server Component
import { db } from '@/lib/db';

interface UserCardProps {
  userId: string;
}

// This is a Server Component by default
export default async function UserCard({ userId }: UserCardProps) {
  // Direct database access in Server Component
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  if (!user) {
    return <div>User not found</div>;
  }

  return (
    <div className="card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <InteractiveButton userId={user.id} />
    </div>
  );
}

// components/ClientComponent.tsx - Client Component
'use client';

import { useState } from 'react';
import styles from './Button.module.css';

interface InteractiveButtonProps {
  userId: string;
}

export function InteractiveButton({ userId }: InteractiveButtonProps) {
  const [isLoading, setIsLoading] = useState(false);
  const [liked, setLiked] = useState(false);

  const handleLike = async () => {
    setIsLoading(true);
    try {
      const response = await fetch(`/api/users/${userId}/like`, {
        method: 'POST',
      });

      if (response.ok) {
        setLiked(!liked);
      }
    } catch (error) {
      console.error('Failed to like user:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <button
      className={`${styles.button} ${liked ? styles.primary : styles.secondary}`}
      onClick={handleLike}
      disabled={isLoading}
    >
      {isLoading ? 'Loading...' : liked ? 'Liked!' : 'Like'}
    </button>
  );
}

// components/CompoundComponent.tsx - Mixed pattern
import { Suspense } from 'react';

export default function UserProfile({ userId }: { userId: string }) {
  return (
    <div className="user-profile">
      {/* Server Component for static data */}
      <Suspense fallback={<UserCardSkeleton />}>
        <UserCard userId={userId} />
      </Suspense>

      {/* Client Component for interactive features */}
      <UserActions userId={userId} />

      {/* Server Component for related data */}
      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts userId={userId} />
      </Suspense>
    </div>
  );
}

Performance & Optimization

Caching, optimization, and performance best practices

Image Optimization

Using Next.js Image component for optimal performance

import Image from 'next/image';

// Basic image optimization
export function ProductCard({ product }: { product: Product }) {
  return (
    <div className="product-card">
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={300}
        height={200}
        className="rounded-lg"
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSd3gqOVSLKGOLKCKLdqDrHO3KGu++5VjSdbdRlKjWWtcjcbBHO/CQcfO+rKvqGq1yp9++2j1ksJZrC5/9k="
        priority // Load above-the-fold images first
      />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
    </div>
  );
}

// Responsive images with multiple sizes
export function HeroImage() {
  return (
    <Image
      src="/hero-image.jpg"
      alt="Hero image"
      fill
      className="object-cover"
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      priority
    />
  );
}

// Dynamic images from CMS or API
export function UserAvatar({ user }: { user: User }) {
  return (
    <Image
      src={user.avatarUrl || '/default-avatar.png'}
      alt={`${user.name}'s avatar`}
      width={40}
      height={40}
      className="rounded-full"
      loading="lazy"
    />
  );
}

// Image gallery with optimization
export function ImageGallery({ images }: { images: string[] }) {
  return (
    <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
      {images.map((src, index) => (
        <div key={src} className="relative aspect-square">
          <Image
            src={src}
            alt={`Gallery image ${index + 1}`}
            fill
            className="object-cover rounded-lg"
            sizes="(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 25vw"
            loading={index < 6 ? 'eager' : 'lazy'}
          />
        </div>
      ))}
    </div>
  );
}

Caching and Revalidation

Data caching, ISR, and on-demand revalidation strategies

// Static data caching (default behavior)
export default async function StaticPage() {
  // This will be cached indefinitely
  const data = await fetch('https://api.example.com/static-data');
  return <div>{/* render data */}</div>;
}

// Time-based revalidation (ISR)
export const revalidate = 3600; // Revalidate every hour

export default async function ISRPage() {
  const data = await fetch('https://api.example.com/posts');
  const posts = await data.json();

  return (
    <div>
      {posts.map((post: any) => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  );
}

// On-demand revalidation
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const secret = searchParams.get('secret');
  const tag = searchParams.get('tag');
  const path = searchParams.get('path');

  // Verify secret token
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
  }

  try {
    if (tag) {
      // Revalidate specific tagged data
      revalidateTag(tag);
    } else if (path) {
      // Revalidate specific path
      revalidatePath(path);
    }

    return NextResponse.json({ revalidated: true });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to revalidate' },
      { status: 500 }
    );
  }
}

// Tagged caching for granular control
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] }
  }).then(res => res.json());

  const featured = await fetch('https://api.example.com/products/featured', {
    next: { tags: ['products', 'featured'] }
  }).then(res => res.json());

  return (
    <div>
      <FeaturedProducts products={featured} />
      <AllProducts products={products} />
    </div>
  );
}

// Webhook to trigger revalidation
// Usage: POST /api/revalidate?secret=token&tag=products

Bundle Optimization

Code splitting, dynamic imports, and bundle analysis

// Dynamic imports for code splitting
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

// Lazy load heavy components
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  ssr: false, // Disable SSR for client-only components
  loading: () => <div>Loading chart...</div>,
});

const AdminPanel = dynamic(() => import('@/components/AdminPanel'), {
  ssr: false,
});

// Conditional loading
export default function Dashboard({ user }: { user: User }) {
  return (
    <div>
      <h1>Dashboard</h1>

      <Suspense fallback={<div>Loading chart...</div>}>
        <HeavyChart data={chartData} />
      </Suspense>

      {user.role === 'admin' && (
        <Suspense fallback={<div>Loading admin panel...</div>}>
          <AdminPanel />
        </Suspense>
      )}
    </div>
  );
}

// Route-based code splitting
const LazyRoute = dynamic(() => import('@/app/heavy-page/page'), {
  loading: () => <div>Loading page...</div>,
});

// Third-party library optimization
import dynamic from 'next/dynamic';

const ReactQuill = dynamic(() => import('react-quill'), {
  ssr: false,
  loading: () => <div>Loading editor...</div>,
});

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

module.exports = withBundleAnalyzer({
  experimental: {
    optimizePackageImports: [
      'lodash',
      'date-fns',
      'react-icons',
    ],
  },

  // Tree shaking optimization
  webpack: (config) => {
    config.optimization.usedExports = true;
    return config;
  },
});

// Package.json scripts for analysis
{
  "scripts": {
    "analyze": "ANALYZE=true npm run build",
    "build:analyze": "npm run build && npx @next/bundle-analyzer",
  }
}