Files

6.9 KiB

name, description
name description
nextjs Use when building Next.js App Router applications — server components, client components, data fetching, routing, middleware, and deployment. Triggers from next.config, layout.tsx, page.tsx, and framework file patterns.

Next.js Best Practices

App Router Architecture

app/
├── (auth)/                  # Route group — no URL segment
│   ├── login/
│   │   └── page.tsx
│   └── register/
│       └── page.tsx
├── (dashboard)/
│   ├── layout.tsx           # Shared layout for all dashboard pages
│   ├── page.tsx             # /dashboard
│   └── settings/
│       └── page.tsx
├── api/                     # API route handlers
│   └── users/
│       └── route.ts
├── layout.tsx               # Root layout
└── page.tsx                 # Home page (/)

Server Components (Default)

Every component in App Router is a Server Component by default. Use Client Components only when needed.

Server components can:

  • async component — async function Page() { const data = await fetch(); ... }
  • Direct database access — await db.select().from(users)
  • Import server-only modules (DB, filesystem, tokens)
  • {children} render client components
// ✅ Server Component — no "use client" directive
export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const user = await db.select().from(users).where(eq(users.id, Number(id))).limit(1);
  if (!user[0]) return notFound();

  return (
    <div>
      <h1>{user[0].name}</h1>
      <UserActions user={user[0]} /> {/* Client component */}
    </div>
  );
}

Client Components — When to Use

Add 'use client' only when you need:

  • 🔴 useState / useReducer — interactive UI state
  • 🔴 useEffect — browser-side effects or synchronization
  • 🔴 useRouter — programmatic navigation
  • 🔴 Event handlers — onClick, onSubmit, onChange
  • 🔴 Browser-only APIs — localStorage, setInterval, IntersectionObserver
  • 🔴 Custom hooks that use any of the above
'use client';

import { useState } from 'react';

export function UserActions({ user }: { user: User }) {
  const [isEditing, setIsEditing] = useState(false);
  return (
    <button onClick={() => setIsEditing(true)}>Edit {user.name}</button>
  );
}

Data Fetching Patterns

Server-side (preferred)

// Direct DB access in server component — no waterfall, no loading states
export default async function Dashboard() {
  const [stats, recentOrders, topUsers] = await Promise.all([
    getStats(), getRecentOrders(), getTopUsers(),
  ]);
  return <DashboardView stats={stats} orders={recentOrders} users={topUsers} />;
}

React Cache (deduplication)

import { cache } from 'react';

export const getItem = cache(async (id: string) => {
  const item = await db.select().from(items).where(eq(items.id, id)).limit(1);
  return item[0];
});

Revalidation

// Time-based (ISR)
export const revalidate = 3600; // seconds

// On-demand
import { revalidatePath, revalidateTag } from 'next/cache';

export async function updateUser(formData: FormData) {
  'use server';
  await db.update(users).set({ name: formData.get('name') }).where(eq(users.id, id));
  revalidatePath(`/users/${id}`); // Revalidate the page
  revalidateTag('users'); // Revalidate all fetch calls with this tag
}

Route Handlers (APIs)

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

const CreateUserSchema = z.object({
  email: z.string().email(),
});

export async function POST(request: NextRequest) {
  const body = await request.json();
  const parsed = CreateUserSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json({ error: parsed.error.flatten() }, { status: 422 });
  }
  const user = await db.insert(users).values(parsed.data).returning();
  return NextResponse.json(user[0], { status: 201 });
}

Server Actions

// app/users/actions.ts
'use server';

import { z } from 'zod';
import { db } from '@/db';
import { users } from '@/db/schema';
import { revalidatePath } from 'next/cache';

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

export async function createUser(formData: FormData) {
  const parsed = CreateUserSchema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { error: parsed.error.flatten() };

  await db.insert(users).values(parsed.data);
  revalidatePath('/users');
  return { success: true };
}

Middleware

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

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value;
  const { pathname } = request.nextUrl;

  // Protected routes
  if (pathname.startsWith('/dashboard') && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Redirect logged-in users away from login
  if (pathname === '/login' && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
};

Performance

  • Server Components over Client Components whenever possible.
  • Streaming — use loading.tsx and Suspense boundaries.
  • Image optimizationnext/image with priority for above-the-fold.
  • Font optimizationnext/font (self-hosted, no layout shift).
  • Bundle analysis@next/bundle-analyzer for tracking bloat.
  • Link prefetch<Link> prefetches by default. Disable with prefetch={false} for low-priority links.

Testing

// Vitest + Testing Library (not Next.js's built-in jest-config)
import { render, screen } from '@testing-library/react';
import Page from './page';

// Mock server component — render with test data
vi.mock('@/db', () => ({ select: () => ({ from: () => ({ where: () => ({ limit: () => [mockUser] }) }) }) }));

describe('UserPage', () => {
  it('renders user name', async () => {
    const page = await Page({ params: Promise.resolve({ id: '1' }) });
    render(page);
    expect(screen.getByText('Alice')).toBeInTheDocument();
  });
});

Anti-patterns

  • 'use client' on every component — most components can be server components
  • Data fetching in client components — prefer server components for data
  • useEffect for data fetching — use server components or TanStack Query
  • Importing server-only code in client components (DB, tokens, env)
  • Large client bundles — lazy load heavy components with next/dynamic
  • Not using notFound() — always handle missing data
  • router.push for navigation that should use <Link> prefetch