NexusCS

Next.js

Web Frameworks
Next.js is a React framework for building full-stack web applications. It provides server-side rendering, static generation, file-based routing, and built-in optimizations.
nextjs
react
server-components
app-router
ssr

Getting started

Installation

npx create-next-app@latest my-app
cd my-app
npm run dev

Open http://localhost:3000 to see your app.

Project structure

app/
├── layout.tsx      # Root layout (required)
├── page.tsx        # Home page
├── loading.tsx     # Loading UI
├── error.tsx       # Error UI
├── not-found.tsx   # 404 UI
├── [slug]/
│   └── page.tsx    # Dynamic route
├── (group)/        # Route group
│   └── page.tsx
├── @modal/         # Parallel route
│   └── page.tsx
└── api/
    └── route.ts    # API route

The app/ directory uses the App Router (recommended for new projects).

Routing patterns

Pattern Description
[slug] Dynamic segment
[...slug] Catch-all segments
[[...slug]] Optional catch-all
(group) Route group (no URL impact)
@slot Parallel routes
(.)folder Intercepting routes

File-based routing automatically creates routes from your folder structure.

Pages & Layouts

Page component

// app/page.tsx - Server Component by default
export default async function Page() {
  const data = await fetch("https://api.example.com/data");
  const json = await data.json();
  return <div>{json.title}</div>;
}

Server Components can use async/await directly.

Dynamic routes

// app/blog/[slug]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <div>Post: {slug}</div>;
}

⚠️ In Next.js 15+, params is a Promise and must be awaited.

Layout component

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

Layouts wrap pages and persist across navigations.

Loading UI

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

Automatically shown while page loads. Uses React Suspense.

Error UI

// app/error.tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Error: {error.message}</h2>
      <button onClick={reset}>Retry</button>
    </div>
  );
}

Error boundaries must be Client Components.

404 UI

// app/not-found.tsx
export default function NotFound() {
  return <div>Page not found</div>;
}

Shown when notFound() is called or route doesn't exist.

Server & Client Components

Server Components

// No directive needed - default
export default async function ServerComponent() {
  const data = await fetch("https://api.example.com");
  const user = await db.users.find();

  return <div>{/* content */}</div>;
}
  • ✅ Direct database access
  • ✅ Async/await anywhere
  • ✅ Keep secrets on server
  • ❌ No React hooks
  • ❌ No browser APIs
  • ❌ No event handlers

Client Components

"use client";

import { useState } from "react";

export default function ClientComponent() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
  • ✅ React hooks
  • ✅ Event handlers
  • ✅ Browser APIs
  • ❌ Direct database access
  • ❌ Server-only modules

Use "use client" directive at the top of the file.

Composition pattern

// Server Component
import ClientComponent from "./ClientComponent";

export default async function Page() {
  const data = await getData();

  return (
    <div>
      <h1>Server content</h1>
      <ClientComponent data={data} />
    </div>
  );
}

Pass server-fetched data as props to client components.

Data Fetching

Server-side fetch

// No caching by default (v15+)
const data = await fetch("https://api.example.com/data");

// With caching (opt-in)
const cached = await fetch(url, {
  cache: "force-cache",
});

// No cache
const fresh = await fetch(url, {
  cache: "no-store",
});

// Revalidation (ISR)
const revalidated = await fetch(url, {
  next: { revalidate: 3600 },
});

⚠️ Fetch is NOT cached by default in Next.js 15+ (changed from v14).

Static generation

export async function generateStaticParams() {
  const posts = await getPosts();

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

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <div>Post: {slug}</div>;
}

Pre-renders pages at build time for dynamic routes.

Server Actions

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

export async function createTodo(formData: FormData) {
  const title = formData.get("title");
  await db.todos.create({ title });
}
// In component
import { createTodo } from "./actions";

export default function Form() {
  return (
    <form action={createTodo}>
      <input name="title" />
      <button type="submit">Add</button>
    </form>
  );
}

Functions that run on the server, callable from client or server.

Revalidation

import { revalidatePath, revalidateTag } from "next/cache";

// Revalidate specific path
revalidatePath("/blog");

// Revalidate by cache tag
revalidateTag("posts");
// Tag cache entries
fetch(url, {
  next: { tags: ["posts"] },
});

Purge cached data on-demand.

Route Handlers

API routes

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

export async function GET(request: NextRequest) {
  const users = await db.users.findMany();
  return NextResponse.json({ users });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await db.users.create(body);
  return NextResponse.json({ user }, { status: 201 });
}

Replace pages/api routes. Export HTTP method functions.

Dynamic routes

// app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const user = await db.users.find(id);

  if (!user) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  return NextResponse.json({ user });
}

Access dynamic segments via params.

Middleware

// 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("token");

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

  return NextResponse.next();
}

export const config = {
  matcher: "/dashboard/:path*",
};

Runs before routes. Use for auth, redirects, rewrites.

Metadata & SEO

Static metadata

// app/page.tsx
export const metadata = {
  title: "My App",
  description: "App description",
  keywords: ["next.js", "react"],
  openGraph: {
    title: "My App",
    description: "App description",
    images: ["/og-image.jpg"],
  },
  twitter: {
    card: "summary_large_image",
    title: "My App",
  },
};

export default function Page() {
  return <div>Content</div>;
}

Define metadata as an exported object.

Dynamic metadata

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

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

Async function for metadata based on route params or data.

Title template

// app/layout.tsx
export const metadata = {
  title: {
    template: "%s | My App",
    default: "My App",
  },
};
// app/about/page.tsx
export const metadata = {
  title: "About", // Becomes "About | My App"
};

Template applies to all child pages.

Components & Hooks

Image component

import Image from 'next/image'

<Image
  src="/photo.jpg"
  alt="Photo"
  width={500}
  height={300}
/>

<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  className="object-cover"
/>

<Image
  src="https://example.com/photo.jpg"
  alt="Remote"
  width={500}
  height={300}
/>

Automatic image optimization. Use fill for responsive images.

Link component

import Link from 'next/link'

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

<Link href="/blog/hello">
  Post
</Link>

<Link
  href="/blog"
  prefetch={false}
>
  Blog
</Link>

<Link
  href={{
    pathname: '/blog/[slug]',
    query: { slug: 'hello' },
  }}
>
  Post
</Link>

Client-side navigation. Auto-prefetches in viewport.

Script component

import Script from 'next/script'

<Script
  src="https://example.com/script.js"
  strategy="lazyOnload"
/>

<Script id="inline-script">
  {`console.log('Hello')`}
</Script>

Optimized script loading. Strategies: beforeInteractive, afterInteractive, lazyOnload.

useRouter

"use client";
import { useRouter } from "next/navigation";

export default function Component() {
  const router = useRouter();

  router.push("/dashboard");
  router.replace("/login");
  router.refresh();
  router.back();
  router.forward();
  router.prefetch("/blog");

  return <button onClick={() => router.push("/about")}>Go to About</button>;
}

Programmatic navigation. Client Component only.

usePathname

"use client";
import { usePathname } from "next/navigation";

export default function Nav() {
  const pathname = usePathname();

  return (
    <nav>
      <a href="/about" className={pathname === "/about" ? "active" : ""}>
        About
      </a>
    </nav>
  );
}

Get current pathname. Returns /about for /about?q=hello.

useSearchParams

"use client";
import { useSearchParams } from "next/navigation";

export default function Search() {
  const searchParams = useSearchParams();
  const query = searchParams.get("q");

  return <div>Search: {query}</div>;
}

Access URL search params. Returns hello for ?q=hello.

Server-side helpers

import { redirect, notFound, cookies, headers } from "next/navigation";

// Redirect
redirect("/login");

// 404
notFound();

// Cookies
const cookieStore = await cookies();
const token = cookieStore.get("token");

// Headers
const headersList = await headers();
const userAgent = headersList.get("user-agent");

⚠️ cookies() and headers() are async in Next.js 15+.

Configuration

next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "**.example.com",
      },
    ],
  },
  async redirects() {
    return [
      {
        source: "/old",
        destination: "/new",
        permanent: true,
      },
    ];
  },
  async rewrites() {
    return [
      {
        source: "/api/:path*",
        destination: "https://api.example.com/:path*",
      },
    ];
  },
};

export default nextConfig;

Configuration for images, redirects, rewrites, and more.

Environment variables

# .env.local (gitignored)
DATABASE_URL=postgresql://...
API_SECRET=secret123

# Public variables (exposed to browser)
NEXT_PUBLIC_API_URL=https://api.example.com
// Server Component
const secret = process.env.API_SECRET;

// Client Component
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

Use NEXT_PUBLIC_ prefix for browser-exposed variables.

Route segment config

// app/page.tsx or app/layout.tsx
export const dynamic = "force-dynamic"; // or 'force-static'
export const revalidate = 3600; // seconds
export const runtime = "edge"; // or 'nodejs'
export const preferredRegion = "auto"; // or ['iad1', 'sfo1']
export const fetchCache = "force-cache";

Configure route behavior per segment.

Build & deployment

# Development
npm run dev

# Production build
npm run build

# Start production server
npm run start

# Static export
npm run build && npm run export
// next.config.ts
const nextConfig = {
  output: "export", // Static HTML export
};

For static export, set output: 'export' in config.

Important Gotchas

Async params (v15+)

// ❌ Wrong (v14 style)
export default function Page({ params }) {
  const { slug } = params;
  return <div>{slug}</div>;
}

// ✅ Correct (v15+)
export default async function Page({ params }) {
  const { slug } = await params;
  return <div>{slug}</div>;
}

params and searchParams are Promises in Next.js 15+.

Async cookies/headers

// ❌ Wrong (v14 style)
const token = cookies().get("token");

// ✅ Correct (v15+)
const cookieStore = await cookies();
const token = cookieStore.get("token");

cookies() and headers() are now async.

Fetch caching

// ❌ Cached by default (v14)
// ✅ NOT cached by default (v15+)
const data = await fetch("https://api.example.com");

// Opt-in to caching
const cached = await fetch(url, { cache: "force-cache" });

Fetch behavior changed in v15. No longer cached by default.

Client boundaries

// ❌ Wrong - entire tree becomes client
"use client";
import ServerComponent from "./ServerComponent";

// ✅ Correct - isolate client component
import ClientComponent from "./ClientComponent";

export default async function Page() {
  return (
    <div>
      <ServerComponent />
      <ClientComponent />
    </div>
  );
}

"use client" makes ALL imports part of client bundle.

Route conflicts

app/
├── dashboard/
│   ├── page.tsx        # ✅ OK
│   └── route.ts        # ❌ Conflict!

page.tsx and route.ts cannot coexist in the same folder.

Also see