Home / Notebooks / Frontend
Frontend
intermediate

Next.js Essentials

Essential Next.js concepts for building modern React applications with server-side rendering and static generation

March 10, 2024
Updated regularly

Next.js Essentials

Quick reference guide for Next.js - the React framework for production.

What is Next.js?

Next.js is a React framework that enables:

  • Server-Side Rendering (SSR) - Render pages on the server
  • Static Site Generation (SSG) - Pre-render pages at build time
  • File-based Routing - Pages created from file system
  • API Routes - Build APIs within your app
  • Automatic Code Splitting - Load only what's needed
  • Image Optimization - Automatic image optimization
  • TypeScript Support - Built-in TypeScript support
  • Fast Refresh - Instant feedback during development
  • Installation

    Create New Project

    # ========== Using create-next-app ==========
    npx create-next-app@latest my-app
    
    # With TypeScript
    npx create-next-app@latest my-app --typescript
    
    # With specific options
    npx create-next-app@latest my-app \
      --typescript \
      --tailwind \
      --app \
      --src-dir
    
    # ========== Start Development Server ==========
    cd my-app
    npm run dev
    
    # Open http://localhost:3000
    

    Manual Installation

    # ========== Install Dependencies ==========
    npm install next@latest react@latest react-dom@latest
    
    # ========== Add Scripts to package.json ==========
    {
      "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
      }
    }
    
    # ========== Create pages Directory ==========
    mkdir pages
    

    Project Structure

    App Router (Next.js 13+)

    my-app/
    ├── app/
    │   ├── layout.tsx          # Root layout
    │   ├── page.tsx            # Home page (/)
    │   ├── loading.tsx         # Loading UI
    │   ├── error.tsx           # Error UI
    │   ├── not-found.tsx       # 404 page
    │   ├── about/
    │   │   └── page.tsx        # About page (/about)
    │   ├── blog/
    │   │   ├── page.tsx        # Blog list (/blog)
    │   │   ├── [slug]/
    │   │   │   └── page.tsx    # Blog post (/blog/[slug])
    │   └── api/
    │       └── hello/
    │           └── route.ts    # API route
    ├── components/
    │   └── Button.tsx
    ├── public/
    │   └── images/
    ├── styles/
    │   └── globals.css
    ├── next.config.js
    ├── package.json
    └── tsconfig.json
    

    Pages Router (Classic)

    my-app/
    ├── pages/
    │   ├── _app.tsx            # Custom App
    │   ├── _document.tsx       # Custom Document
    │   ├── index.tsx           # Home page (/)
    │   ├── about.tsx           # About page (/about)
    │   ├── blog/
    │   │   ├── index.tsx       # Blog list (/blog)
    │   │   └── [slug].tsx      # Blog post (/blog/[slug])
    │   └── api/
    │       └── hello.ts        # API route
    ├── components/
    ├── public/
    ├── styles/
    ├── next.config.js
    └── package.json
    

    Routing (App Router)

    Pages

    // ========== app/page.tsx (Home Page) ==========
    export default function HomePage() {
      return (
        <main>
          <h1>Welcome to Next.js</h1>
          <p>This is the home page</p>
        </main>
      );
    }
    
    // ========== app/about/page.tsx ==========
    export default function AboutPage() {
      return (
        <div>
          <h1>About Us</h1>
          <p>Learn more about our company</p>
        </div>
      );
    }
    
    // ========== app/blog/[slug]/page.tsx (Dynamic Route) ==========
    export default function BlogPost({ params }: { params: { slug: string } }) {
      return (
        <article>
          <h1>Blog Post: {params.slug}</h1>
          <p>Content for {params.slug}</p>
        </article>
      );
    }
    
    // ========== app/shop/[...slug]/page.tsx (Catch-all Route) ==========
    // Matches /shop/a, /shop/a/b, /shop/a/b/c, etc.
    export default function ShopPage({ params }: { params: { slug: string[] } }) {
      return (
        <div>
          <h1>Shop</h1>
          <p>Path: {params.slug.join('/')}</p>
        </div>
      );
    }
    

    Layouts

    // ========== app/layout.tsx (Root Layout) ==========
    import './globals.css';
    import { Inter } from 'next/font/google';
    
    const inter = Inter({ subsets: ['latin'] });
    
    export const metadata = {
      title: 'My App',
      description: 'Built with Next.js',
    };
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang="en">
          <body className={inter.className}>
            <header>
              <nav>Navigation</nav>
            </header>
            {children}
            <footer>Footer</footer>
          </body>
        </html>
      );
    }
    
    // ========== app/dashboard/layout.tsx (Nested Layout) ==========
    export default function DashboardLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <div className="dashboard">
          <aside>
            <nav>Dashboard Navigation</nav>
          </aside>
          <main>{children}</main>
        </div>
      );
    }
    
    // ========== Using Link ==========
    import Link from 'next/link';
    
    export default function Navigation() {
      return (
        <nav>
          <Link href="/">Home</Link>
          <Link href="/about">About</Link>
          <Link href="/blog">Blog</Link>
          
          {/* With dynamic route */}
          <Link href={`/blog/${slug}`}>Read Post</Link>
          
          {/* With query parameters */}
          <Link href="/search?q=nextjs">Search</Link>
          
          {/* With object */}
          <Link href={{ pathname: '/blog', query: { page: 1 } }}>
            Blog Page 1
          </Link>
        </nav>
      );
    }
    
    // ========== Programmatic Navigation ==========
    'use client';
    
    import { useRouter } from 'next/navigation';
    
    export default function MyComponent() {
      const router = useRouter();
      
      const handleClick = () => {
        router.push('/dashboard');
        // router.replace('/dashboard'); // Without adding to history
        // router.back(); // Go back
        // router.forward(); // Go forward
        // router.refresh(); // Refresh current route
      };
      
      return <button onClick={handleClick}>Go to Dashboard</button>;
    }
    

    Data Fetching (App Router)

    Server Components (Default)

    // ========== Static Data Fetching ==========
    // app/posts/page.tsx
    async function getPosts() {
      const res = await fetch('https://api.example.com/posts', {
        cache: 'force-cache' // Default: cache forever
      });
      
      if (!res.ok) {
        throw new Error('Failed to fetch posts');
      }
      
      return res.json();
    }
    
    export default async function PostsPage() {
      const posts = await getPosts();
      
      return (
        <div>
          <h1>Posts</h1>
          <ul>
            {posts.map((post) => (
              <li key={post.id}>{post.title}</li>
            ))}
          </ul>
        </div>
      );
    }
    
    // ========== Dynamic Data Fetching ==========
    async function getPost(id: string) {
      const res = await fetch(`https://api.example.com/posts/${id}`, {
        cache: 'no-store' // Always fetch fresh data
      });
      
      return res.json();
    }
    
    // ========== Revalidation ==========
    async function getPosts() {
      const res = await fetch('https://api.example.com/posts', {
        next: { revalidate: 3600 } // Revalidate every hour
      });
      
      return res.json();
    }
    
    // ========== Multiple Data Sources ==========
    export default async function Page() {
      // Fetch in parallel
      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 (
        <div>
          <Posts data={posts} />
          <Users data={users} />
        </div>
      );
    }
    

    Client Components

    // ========== Use Client Directive ==========
    'use client';
    
    import { useState, useEffect } from 'react';
    
    export default function ClientComponent() {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      
      useEffect(() => {
        fetch('/api/data')
          .then(res => res.json())
          .then(data => {
            setData(data);
            setLoading(false);
          });
      }, []);
      
      if (loading) return <div>Loading...</div>;
      
      return <div>{JSON.stringify(data)}</div>;
    }
    
    // ========== With SWR ==========
    'use client';
    
    import useSWR from 'swr';
    
    const fetcher = (url: string) => fetch(url).then(r => r.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>;
    }
    

    Static Generation

    // ========== Generate Static Params ==========
    // app/blog/[slug]/page.tsx
    export async function generateStaticParams() {
      const posts = await fetch('https://api.example.com/posts').then(r => r.json());
      
      return posts.map((post) => ({
        slug: post.slug,
      }));
    }
    
    export default async function Post({ params }: { params: { slug: string } }) {
      const post = await fetch(`https://api.example.com/posts/${params.slug}`)
        .then(r => r.json());
      
      return (
        <article>
          <h1>{post.title}</h1>
          <p>{post.content}</p>
        </article>
      );
    }
    
    // ========== Generate Metadata ==========
    export async function generateMetadata({ params }: { params: { slug: string } }) {
      const post = await fetch(`https://api.example.com/posts/${params.slug}`)
        .then(r => r.json());
      
      return {
        title: post.title,
        description: post.excerpt,
        openGraph: {
          title: post.title,
          description: post.excerpt,
          images: [post.image],
        },
      };
    }
    

    API Routes

    App Router API Routes

    // ========== app/api/hello/route.ts ==========
    import { NextResponse } from 'next/server';
    
    export async function GET(request: Request) {
      return NextResponse.json({ message: 'Hello World' });
    }
    
    export async function POST(request: Request) {
      const body = await request.json();
      
      return NextResponse.json({ 
        message: 'Data received',
        data: body 
      });
    }
    
    // ========== With Dynamic Route ==========
    // app/api/posts/[id]/route.ts
    export async function GET(
      request: Request,
      { params }: { params: { id: string } }
    ) {
      const id = params.id;
      
      const post = await db.posts.findById(id);
      
      if (!post) {
        return NextResponse.json(
          { error: 'Post not found' },
          { status: 404 }
        );
      }
      
      return NextResponse.json(post);
    }
    
    export async function PUT(
      request: Request,
      { params }: { params: { id: string } }
    ) {
      const body = await request.json();
      const updated = await db.posts.update(params.id, body);
      
      return NextResponse.json(updated);
    }
    
    export async function DELETE(
      request: Request,
      { params }: { params: { id: string } }
    ) {
      await db.posts.delete(params.id);
      
      return NextResponse.json({ success: true });
    }
    
    // ========== With Query Parameters ==========
    export async function GET(request: Request) {
      const { searchParams } = new URL(request.url);
      const query = searchParams.get('q');
      const page = searchParams.get('page') || '1';
      
      const results = await search(query, parseInt(page));
      
      return NextResponse.json(results);
    }
    
    // ========== With Headers ==========
    export async function GET(request: Request) {
      const token = request.headers.get('authorization');
      
      if (!token) {
        return NextResponse.json(
          { error: 'Unauthorized' },
          { status: 401 }
        );
      }
      
      const data = await fetchProtectedData(token);
      
      return NextResponse.json(data, {
        headers: {
          'Cache-Control': 'no-store',
          'X-Custom-Header': 'value'
        }
      });
    }
    
    // ========== With Cookies ==========
    import { cookies } from 'next/headers';
    
    export async function GET() {
      const cookieStore = cookies();
      const token = cookieStore.get('token');
      
      return NextResponse.json({ token: token?.value });
    }
    
    export async function POST(request: Request) {
      const response = NextResponse.json({ success: true });
      
      response.cookies.set('token', 'abc123', {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        maxAge: 3600
      });
      
      return response;
    }
    

    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) {
        return NextResponse.redirect(new URL('/login', request.url));
      }
      
      // Add custom header
      const response = NextResponse.next();
      response.headers.set('X-Custom-Header', 'value');
      
      return response;
    }
    
    // ========== Configure Matcher ==========
    export const config = {
      matcher: [
        '/dashboard/:path*',
        '/api/:path*',
      ]
    };
    
    // ========== Rewrite ==========
    export function middleware(request: NextRequest) {
      // Rewrite /old-path to /new-path
      if (request.nextUrl.pathname === '/old-path') {
        return NextResponse.rewrite(new URL('/new-path', request.url));
      }
    }
    
    // ========== Logging ==========
    export function middleware(request: NextRequest) {
      console.log('Request:', request.method, request.url);
      return NextResponse.next();
    }
    

    Image Optimization

    // ========== Next.js Image Component ==========
    import Image from 'next/image';
    
    export default function MyComponent() {
      return (
        <div>
          {/* Static import */}
          <Image
            src="/images/hero.jpg"
            alt="Hero image"
            width={1200}
            height={600}
            priority // Load immediately
          />
          
          {/* External image */}
          <Image
            src="https://example.com/image.jpg"
            alt="External image"
            width={800}
            height={400}
            loading="lazy"
          />
          
          {/* Fill container */}
          <div style={{ position: 'relative', width: '100%', height: '400px' }}>
            <Image
              src="/images/background.jpg"
              alt="Background"
              fill
              style={{ objectFit: 'cover' }}
            />
          </div>
          
          {/* Responsive */}
          <Image
            src="/images/responsive.jpg"
            alt="Responsive image"
            width={1200}
            height={600}
            sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
          />
        </div>
      );
    }
    
    // ========== Configure External Images ==========
    // next.config.js
    module.exports = {
      images: {
        domains: ['example.com', 'cdn.example.com'],
        remotePatterns: [
          {
            protocol: 'https',
            hostname: '**.example.com',
          },
        ],
      },
    };
    

    Loading and Error States

    // ========== app/loading.tsx ==========
    export default function Loading() {
      return (
        <div className="spinner">
          <p>Loading...</p>
        </div>
      );
    }
    
    // ========== app/error.tsx ==========
    'use client';
    
    export default function Error({
      error,
      reset,
    }: {
      error: Error;
      reset: () => void;
    }) {
      return (
        <div>
          <h2>Something went wrong!</h2>
          <p>{error.message}</p>
          <button onClick={reset}>Try again</button>
        </div>
      );
    }
    
    // ========== app/not-found.tsx ==========
    export default function NotFound() {
      return (
        <div>
          <h2>404 - Page Not Found</h2>
          <p>The page you're looking for doesn't exist.</p>
        </div>
      );
    }
    
    // ========== Custom 404 in Page ==========
    import { notFound } from 'next/navigation';
    
    export default async function Page({ params }: { params: { id: string } }) {
      const data = await fetchData(params.id);
      
      if (!data) {
        notFound();
      }
      
      return <div>{data.title}</div>;
    }
    

    Streaming and Suspense

    // ========== app/page.tsx ==========
    import { Suspense } from 'react';
    
    async function SlowComponent() {
      await new Promise(resolve => setTimeout(resolve, 3000));
      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>
          
          <h2>More fast content</h2>
        </div>
      );
    }
    
    // ========== Multiple Suspense Boundaries ==========
    export default function Dashboard() {
      return (
        <div>
          <h1>Dashboard</h1>
          
          <Suspense fallback={<Skeleton />}>
            <RevenueChart />
          </Suspense>
          
          <Suspense fallback={<Skeleton />}>
            <RecentOrders />
          </Suspense>
          
          <Suspense fallback={<Skeleton />}>
            <TopProducts />
          </Suspense>
        </div>
      );
    }
    

    Server Actions

    // ========== Server Action ==========
    'use server';
    
    import { revalidatePath } from 'next/cache';
    
    export async function createPost(formData: FormData) {
      const title = formData.get('title');
      const content = formData.get('content');
      
      const post = await db.posts.create({
        title,
        content,
      });
      
      revalidatePath('/posts');
      
      return { success: true, id: post.id };
    }
    
    // ========== Use in Client Component ==========
    'use client';
    
    import { createPost } from './actions';
    
    export default function CreatePostForm() {
      return (
        <form action={createPost}>
          <input name="title" type="text" required />
          <textarea name="content" required />
          <button type="submit">Create Post</button>
        </form>
      );
    }
    
    // ========== With useFormState ==========
    'use client';
    
    import { useFormState } from 'react-dom';
    import { createPost } from './actions';
    
    export default function Form() {
      const [state, formAction] = useFormState(createPost, null);
      
      return (
        <form action={formAction}>
          <input name="title" />
          <button type="submit">Submit</button>
          {state?.error && <p>{state.error}</p>}
        </form>
      );
    }
    

    Environment Variables

    # ========== .env.local ==========
    # Public variables (exposed to browser)
    NEXT_PUBLIC_API_URL=https://api.example.com
    NEXT_PUBLIC_GA_ID=UA-123456789-1
    
    # Private variables (server-side only)
    DATABASE_URL=postgresql://localhost/mydb
    API_SECRET_KEY=secret123
    
    // ========== Usage ==========
    // Client-side
    const apiUrl = process.env.NEXT_PUBLIC_API_URL;
    
    // Server-side only
    const dbUrl = process.env.DATABASE_URL;
    
    // ========== Type-safe Environment Variables ==========
    // env.d.ts
    declare namespace NodeJS {
      interface ProcessEnv {
        NEXT_PUBLIC_API_URL: string;
        DATABASE_URL: string;
        API_SECRET_KEY: string;
      }
    }
    

    Configuration

    // ========== next.config.js ==========
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      // React strict mode
      reactStrictMode: true,
      
      // Image optimization
      images: {
        domains: ['example.com'],
        formats: ['image/avif', 'image/webp'],
      },
      
      // Redirects
      async redirects() {
        return [
          {
            source: '/old-path',
            destination: '/new-path',
            permanent: true,
          },
        ];
      },
      
      // Rewrites
      async rewrites() {
        return [
          {
            source: '/api/:path*',
            destination: 'https://api.example.com/:path*',
          },
        ];
      },
      
      // Headers
      async headers() {
        return [
          {
            source: '/api/:path*',
            headers: [
              { key: 'Access-Control-Allow-Origin', value: '*' },
            ],
          },
        ];
      },
      
      // Environment variables
      env: {
        CUSTOM_KEY: 'value',
      },
      
      // Webpack configuration
      webpack: (config, { isServer }) => {
        // Custom webpack config
        return config;
      },
    };
    
    module.exports = nextConfig;
    

    Metadata and SEO

    // ========== Static Metadata ==========
    // app/page.tsx
    import type { Metadata } from 'next';
    
    export const metadata: Metadata = {
      title: 'My Page',
      description: 'Page description',
      keywords: ['nextjs', 'react', 'ssr'],
      authors: [{ name: 'John Doe' }],
      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'],
      },
    };
    
    // ========== Dynamic Metadata ==========
    export async function generateMetadata({ params }): Promise<Metadata> {
      const post = await fetchPost(params.id);
      
      return {
        title: post.title,
        description: post.excerpt,
        openGraph: {
          title: post.title,
          description: post.excerpt,
          images: [post.coverImage],
        },
      };
    }
    
    // ========== Template Metadata ==========
    // app/layout.tsx
    export const metadata: Metadata = {
      title: {
        template: '%s | My Site',
        default: 'My Site',
      },
    };
    
    // app/about/page.tsx
    export const metadata: Metadata = {
      title: 'About', // Becomes "About | My Site"
    };
    

    Authentication Example

    // ========== lib/auth.ts ==========
    import { cookies } from 'next/headers';
    import { redirect } from 'next/navigation';
    import { jwtVerify } from 'jose';
    
    export async function getSession() {
      const token = cookies().get('session')?.value;
      
      if (!token) return null;
      
      try {
        const { payload } = await jwtVerify(
          token,
          new TextEncoder().encode(process.env.JWT_SECRET)
        );
        return payload;
      } catch {
        return null;
      }
    }
    
    export async function requireAuth() {
      const session = await getSession();
      
      if (!session) {
        redirect('/login');
      }
      
      return session;
    }
    
    // ========== app/dashboard/page.tsx ==========
    import { requireAuth } from '@/lib/auth';
    
    export default async function DashboardPage() {
      const session = await requireAuth();
      
      return (
        <div>
          <h1>Dashboard</h1>
          <p>Welcome, {session.email}</p>
        </div>
      );
    }
    
    // ========== app/api/auth/login/route.ts ==========
    import { SignJWT } from 'jose';
    import { NextResponse } from 'next/server';
    
    export async function POST(request: Request) {
      const { email, password } = await request.json();
      
      // Verify credentials
      const user = await verifyCredentials(email, password);
      
      if (!user) {
        return NextResponse.json(
          { error: 'Invalid credentials' },
          { status: 401 }
        );
      }
      
      // Create JWT
      const token = await new SignJWT({ userId: user.id, email: user.email })
        .setProtectedHeader({ alg: 'HS256' })
        .setExpirationTime('24h')
        .sign(new TextEncoder().encode(process.env.JWT_SECRET));
      
      const response = NextResponse.json({ success: true });
      
      response.cookies.set('session', token, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        maxAge: 86400, // 24 hours
      });
      
      return response;
    }
    

    Performance Optimization

    Code Splitting

    // ========== Dynamic Import ==========
    import dynamic from 'next/dynamic';
    
    const DynamicComponent = dynamic(() => import('@/components/Heavy'), {
      loading: () => <p>Loading...</p>,
      ssr: false, // Disable SSR for this component
    });
    
    export default function Page() {
      return (
        <div>
          <DynamicComponent />
        </div>
      );
    }
    
    // ========== Lazy Load with Suspense ==========
    import { lazy, Suspense } from 'react';
    
    const LazyComponent = lazy(() => import('@/components/Heavy'));
    
    export default function Page() {
      return (
        <Suspense fallback={<div>Loading...</div>}>
          <LazyComponent />
        </Suspense>
      );
    }
    

    Font Optimization

    // ========== Google Fonts ==========
    import { Inter, Roboto_Mono } from 'next/font/google';
    
    const inter = Inter({
      subsets: ['latin'],
      display: 'swap',
    });
    
    const robotoMono = Roboto_Mono({
      subsets: ['latin'],
      weight: ['400', '700'],
    });
    
    export default function RootLayout({ children }) {
      return (
        <html lang="en" className={inter.className}>
          <body>{children}</body>
        </html>
      );
    }
    
    // ========== Local Fonts ==========
    import localFont from 'next/font/local';
    
    const myFont = localFont({
      src: './my-font.woff2',
      display: 'swap',
    });
    

    Deployment

    Build and Export

    # ========== Build for Production ==========
    npm run build
    
    # ========== Start Production Server ==========
    npm start
    
    # ========== Static Export ==========
    # next.config.js
    module.exports = {
      output: 'export',
    };
    
    npm run build
    # Output in 'out' directory
    

    Vercel Deployment

    # ========== Install Vercel CLI ==========
    npm i -g vercel
    
    # ========== Deploy ==========
    vercel
    
    # ========== Production Deployment ==========
    vercel --prod
    

    Docker Deployment

    # ========== Dockerfile ==========
    FROM node:18-alpine AS builder
    
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
    COPY . .
    RUN npm run build
    
    FROM node:18-alpine 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
    ENV PORT 3000
    
    CMD ["node", "server.js"]
    

    Best Practices

  • Use Server Components by default - Add 'use client' only when needed
  • Fetch data at the highest level - Reduce waterfalls
  • Use Suspense for streaming - Improve perceived performance
  • Optimize images - Always use next/image
  • Implement proper loading states - Use loading.tsx
  • Handle errors gracefully - Use error.tsx
  • Use TypeScript - Type safety and better DX
  • Configure metadata - Improve SEO
  • Use environment variables - Secure sensitive data
  • Monitor performance - Use Next.js Speed Insights
  • Common Patterns

    Protected Routes

    // ========== HOC for Auth ==========
    import { redirect } from 'next/navigation';
    import { getSession } from '@/lib/auth';
    
    export default async function ProtectedPage() {
      const session = await getSession();
      
      if (!session) {
        redirect('/login');
      }
      
      return <div>Protected content</div>;
    }
    

    Pagination

    // ========== app/posts/page.tsx ==========
    export default async function PostsPage({
      searchParams,
    }: {
      searchParams: { page?: string };
    }) {
      const page = parseInt(searchParams.page || '1');
      const limit = 10;
      
      const posts = await fetchPosts(page, limit);
      const total = await getPostCount();
      const totalPages = Math.ceil(total / limit);
      
      return (
        <div>
          <PostList posts={posts} />
          <Pagination currentPage={page} totalPages={totalPages} />
        </div>
      );
    }
    
    // ========== app/search/page.tsx ==========
    export default async function SearchPage({
      searchParams,
    }: {
      searchParams: { q?: string };
    }) {
      const query = searchParams.q || '';
      const results = query ? await search(query) : [];
      
      return (
        <div>
          <SearchForm defaultValue={query} />
          <SearchResults results={results} />
        </div>
      );
    }
    

    Resources

  • Next.js Documentation
  • Next.js Learn
  • Next.js Examples
  • App Router Playground
  • Vercel
  • Topics

    Next.jsReactSSRSSGTypeScript

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.