nextjs

SKILL.md

Next.js Framework Guide

Framework: Next.js 14+ (App Router) Language: TypeScript/JavaScript Use Cases: Full-Stack Web Apps, SSR/SSG, E-commerce, Blogs, Dashboards

Overview

Next.js is a React framework providing server-side rendering, static site generation, API routes, and full-stack development in a single codebase. Version 14+ uses the App Router as the default, built on React Server Components.

Project Setup

# Create new Next.js app
npx create-next-app@latest my-app --typescript --tailwind --eslint --app

cd my-app
npm run dev

Recommended Project Structure

my-app/
├── app/
│   ├── (auth)/                 # Route group (no URL segment)
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   ├── dashboard/
│   │   ├── page.tsx            # /dashboard
│   │   ├── loading.tsx         # Loading UI
│   │   ├── error.tsx           # Error boundary
│   │   └── layout.tsx          # Dashboard layout
│   ├── api/
│   │   └── users/route.ts     # API route handler
│   ├── globals.css
│   ├── layout.tsx              # Root layout (required)
│   └── page.tsx                # Home page (/)
├── components/
│   ├── ui/                     # Reusable UI components
│   └── features/               # Feature-specific components
├── lib/
│   ├── db.ts                   # Database client
│   └── utils.ts                # Utility functions
├── hooks/                      # Custom React hooks
├── types/                      # TypeScript type definitions
├── public/                     # Static assets
├── middleware.ts               # Edge middleware
├── next.config.js
├── tailwind.config.ts
└── package.json

Routing (App Router)

File-Based Routing Conventions

File Purpose
page.tsx Route UI (makes segment publicly accessible)
layout.tsx Shared layout (wraps children, persists)
loading.tsx Loading UI (Suspense boundary)
error.tsx Error boundary (must be 'use client')
not-found.tsx 404 UI for this segment
route.ts API route handler (GET, POST, etc.)
template.tsx Like layout but re-mounts on navigation
default.tsx Fallback for parallel routes

Route Patterns

app/
├── page.tsx                    # /
├── about/page.tsx              # /about
├── blog/
│   ├── page.tsx                # /blog
│   └── [slug]/page.tsx         # /blog/:slug (dynamic)
├── shop/
│   └── [...categories]/page.tsx  # /shop/a/b/c (catch-all)
├── (marketing)/                # Route group (no URL impact)
│   ├── pricing/page.tsx        # /pricing
│   └── features/page.tsx       # /features
└── @modal/                     # Parallel route (named slot)
    └── login/page.tsx

Page Component with Params

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

export default function BlogPost({ params, searchParams }: PageProps) {
  return <article><h1>Post: {params.slug}</h1></article>;
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const post = await getPost(params.slug);
  return { title: post.title, description: post.excerpt };
}

Layouts

// app/layout.tsx -- Root Layout (required, wraps entire app)
import { Inter } from 'next/font/google';

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

export const metadata: Metadata = {
  title: { default: 'My App', template: '%s | My App' },
  description: 'My application',
};

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

Nested layouts compose automatically. Dashboard layout wraps all /dashboard/* routes:

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <Sidebar />
      <div className="flex-1">{children}</div>
    </div>
  );
}

Server Components vs Client Components

Decision Rule

Need Component Type
Fetch data, access backend resources Server (default)
Static rendering, SEO content Server
Use hooks (useState, useEffect, etc.) Client
Browser APIs (window, localStorage) Client
Event handlers (onClick, onChange) Client
Third-party client-only libraries Client

Server Component (Default)

All components in the app/ directory are Server Components by default. They run on the server only and can directly access databases, file systems, and secrets.

// app/users/page.tsx -- Server Component (no directive needed)
import { db } from '@/lib/db';

export default async function UsersPage() {
  const users = await db.user.findMany();
  return (
    <ul>
      {users.map((user) => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Client Component

Add 'use client' at the top of the file. Push this directive as low in the tree as possible.

// components/Counter.tsx
'use client';

import { useState } from 'react';

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

Composition Pattern

Fetch data in Server Components, pass to Client Components as props:

// app/dashboard/page.tsx (Server Component)
import { ClientSidebar } from '@/components/ClientSidebar';
import { db } from '@/lib/db';

export default async function Dashboard() {
  const stats = await db.stats.get();
  return (
    <div>
      <ClientSidebar initialStats={stats} />
      <DashboardContent stats={stats} />
    </div>
  );
}

Data Fetching

Server Component Fetch with Caching

async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // ISR: revalidate every hour
  });
  if (!res.ok) throw new Error('Failed to fetch products');
  return res.json();
}

Fetch Caching Options

Option Behavior
{ cache: 'force-cache' } Static (default for GET)
{ cache: 'no-store' } Dynamic (no caching)
{ next: { revalidate: N } } ISR (revalidate every N seconds)
{ next: { tags: ['posts'] } } Tag-based revalidation

Parallel Fetching

Always fetch independent data in parallel with Promise.all:

export default async function Dashboard({ params }: { params: { id: string } }) {
  const [user, orders] = await Promise.all([
    getUser(params.id),
    getOrders(params.id),
  ]);
  return <div><UserProfile user={user} /><OrderList orders={orders} /></div>;
}

Streaming with Suspense

import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <WelcomeMessage />
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

Server Actions

Define mutations with 'use server'. They run on the server and can be called from forms or client code.

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

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';

const createPostSchema = z.object({
  title: z.string().min(1),
  content: z.string().min(10),
});

export async function createPost(formData: FormData) {
  const validated = createPostSchema.parse({
    title: formData.get('title'),
    content: formData.get('content'),
  });
  await db.post.create({ data: validated });
  revalidatePath('/posts');
  redirect(`/posts`);
}

Use in a form (no client JavaScript required for basic submissions):

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create</button>
    </form>
  );
}

API Route Handlers

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

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

export async function GET(request: NextRequest) {
  const page = parseInt(request.nextUrl.searchParams.get('page') || '1');
  const users = await db.user.findMany({ skip: (page - 1) * 10, take: 10 });
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validated = userSchema.parse(body);
    const user = await db.user.create({ data: validated });
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json({ errors: error.errors }, { status: 400 });
    }
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}

Middleware

Runs at the edge before every matched request. Use for auth checks, redirects, headers.

// middleware.ts (project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

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

  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

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

Error Handling

Error Boundary (error.tsx)

// app/dashboard/error.tsx
'use client';

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <h2>Something went wrong</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Not Found

// app/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <Link href="/">Return Home</Link>
    </div>
  );
}

Trigger programmatically: import { notFound } from 'next/navigation'; notFound();

Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }],
  },
  async redirects() {
    return [{ source: '/old', destination: '/new', permanent: true }];
  },
  async headers() {
    return [{
      source: '/api/:path*',
      headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }],
    }];
  },
};
module.exports = nextConfig;

Guardrails

  • Use Server Components by default; add 'use client' only when needed
  • Push 'use client' as low in the component tree as possible
  • Colocate data fetching with the component that needs it
  • Use Promise.all for independent parallel fetches
  • Implement loading.tsx and error.tsx for every route segment
  • Use Server Actions for mutations (not API routes for form submissions)
  • Validate all inputs with schema validators (Zod) in Server Actions and API routes
  • Use next/image for images and next/font for fonts (performance)
  • Set proper metadata on every page for SEO
  • Use Suspense boundaries to stream slow data
  • Never import server-only modules in Client Components
  • Never expose secrets or database access in Client Components

Commands Reference

npm run dev          # Development server (http://localhost:3000)
npm run build        # Production build
npm run start        # Start production server
npm run lint         # ESLint check
npx tsc --noEmit     # TypeScript validation
npm test             # Run tests (Vitest/Jest)

Advanced Topics

For detailed code examples, advanced patterns, testing strategies, performance optimization, caching strategies, and ISR/SSG/SSR details, see:

  • references/patterns.md -- Authentication, advanced Server Actions, testing, performance, caching, rendering strategies, deployment

External References

Weekly Installs
9
Repository
ar4mirez/samuel
GitHub Stars
3
First Seen
Feb 20, 2026
Installed on
opencode9
gemini-cli9
github-copilot9
codex9
amp9
kimi-cli9