DevTools

Cheatsheet Next.js

Framework React full-stack com renderização no servidor e App Router

Back to languages
Next.js
85 cards found
Categories:
Versions:

Setup e CLI


9 cards
Create Project
npx create-next-app@latest my-app
cd my-app
npm run dev

create-next-app generates a complete project with App Router, TypeScript and ESLint. npm run dev starts the development server on localhost:3000 with hot reload.

TypeScript
// app/page.tsx
export default function Home() {
  return <h1>Hello</h1>;
}

// Types for params:
export default function Post({
  params,
}: { params: { slug: string } }) {}

Next.js has native TypeScript support. .tsx files for components with JSX. The tsconfig.json is generated automatically. Types for params and searchParams are inferred.

ESLint and Prettier
// .eslintrc.json
{
  "extends": "next/core-web-vitals"
}

// next lint detects:
// - hooks rules
// - image optimization
// - link issues

next/core-web-vitals includes React + Next.js rules. Detects incorrect use of Image, Link and hooks. npm run lint checks the whole project.

CLI Commands
npm run dev      # development
npm run build    # production build
npm run start    # serve the build
npm run lint     # check code

dev for development with HMR. build compiles and optimizes for production. start serves the build. lint runs the configured ESLint.

Environment Variables
// .env.local
DATABASE_URL=postgres://...
NEXT_PUBLIC_API_URL=https://api.example.com

// In code:
process.env.DATABASE_URL       // server
process.env.NEXT_PUBLIC_API_URL // client

.env.local stores secrets (not committed to git). Variables with the NEXT_PUBLIC_ prefix are available on the client. Without the prefix, they are only accessible on the server.

App Router Structure
app/
  layout.tsx      # root layout
  page.tsx        # home page (/)
  globals.css     # global styles
public/           # static files
next.config.js    # configuration

The app/ folder defines routes by files. layout.tsx wraps all pages. public/ serves static files at the root. next.config.js customizes the build.

Path Aliases
// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

// Usage:
import { Card } from "@/components/Card";

@/ is the default alias for src/ (or the root). Configured in tsconfig.json with paths. Avoids long relative imports like ../../../components.

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ["example.com"],
  },
};

module.exports = nextConfig;

next.config.js configures the framework. reactStrictMode enables extra checks. images.domains allows external domains for the Image component.

src Directory
src/
  app/
    layout.tsx
    page.tsx
  components/
    Card.tsx
  lib/
    utils.ts

The src/ folder is optional — it organizes source code separately from configs. If it exists, the App Router looks for src/app/ instead of app/. Recommended for large projects.

App Router and Pages


10 cards
Basic Page
// app/about/page.tsx
export default function About() {
  return <h1>About us</h1>;
}

Each folder with a page.tsx becomes a route. app/about/page.tsx/about. The component is exported the default. By default, it is a Server Component.

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

loading.tsx shows a fallback while the page loads. It automatically wraps the page in Suspense. It appears instantly on navigation — improves perceived UX.

Route Groups
app/
  (marketing)/
    layout.tsx
    about/page.tsx
  (app)/
    layout.tsx
    dashboard/page.tsx

Parentheses (name) create route groups without affecting the URL. Each group can have its own layout. /about and /dashboard live in different layouts without a URL prefix.

Root Route
// app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Welcome</h1>
    </main>
  );
}

app/page.tsx is the home page (/). It is the application entry point. It automatically inherits the root layout from app/layout.tsx.

error.tsx
"use client";

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

error.tsx catches runtime errors in the section. It must be a Client Component ("use client"). reset() tries to re-render. Works the an automatic error boundary.

Parallel and Intercepting Routes
app/
  @modal/
    (.)photo/[id]/page.tsx
  layout.tsx    # receives {modal, children}
  page.tsx

// layout.tsx:
export default function Layout({
  children, modal
}) { return <>{children}{modal}</> }

@slot defines parallel routes (multiple pages in the same layout). (.) intercepts routes (modal over the current page). The pattern for Instagram/Twitter-style modals.

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

The root layout is required and wraps all pages. It must contain <html> and <body>. children is the child page or layout. It persists between navigations (no re-mount).

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

not-found.tsx renders when a route does not exist or when notFound() is called manually. It can exist globally or per section. Returns HTTP status 404.

Nested Layout
// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: { children: React.ReactNode }) {
  return (
    <div>
      <nav>Blog menu</nav>
      {children}
    </div>
  );
}

Layouts in subfolders apply to all routes in that section. app/blog/layout.tsx wraps /blog and /blog/[slug]. Layouts nest hierarchically without re-rendering on navigation.

template.tsx
// app/template.tsx
export default function Template({
  children,
}: { children: React.ReactNode }) {
  return <div className="animate">{children}</div>;
}

template.tsx is like a layout but is recreated on every navigation (new mount). It does not preserve state. Useful for entry animations or effects that should restart on each route.

Components and Layout


9 cards
Static Metadata
// app/layout.tsx or page.tsx
export const metadata = {
  title: "My Site",
  description: "Site description",
  openGraph: {
    title: "My Site",
    images: ["/og.png"],
  },
};

metadata defines SEO and Open Graph. Exported the a constant in Server Components. Automatically generates <title>, <meta> and og: tags in the <head>.

next/image
import Image from "next/image";

<Image
  src="/photo.jpg"
  width={800}
  height={600}
  alt="Description"
  priority
/>

next/image optimizes images: resize, lazy load, modern formats (WebP/AVIF). width/height prevent layout shift. priority disables lazy load for above-the-fold images.

global-error and Sitemap
// app/global-error.tsx ("use client")
export default function GlobalError() {
  return <html><body><h1>Error</h1></body></html>;
}

// app/sitemap.ts
export default function sitemap() {
  return [{ url: "https://site.com", lastModified: new Date() }];
}

global-error.tsx replaces the root layout on fatal errors. sitemap.ts generates /sitemap.xml dynamically. Both are App Router convention files.

Dynamic Metadata
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return { title: post.title };
}

generateMetadata() generates dynamic metadata per page. It receives params and searchParams. Lets you fetch data and set title, description, etc. per route.

next/link
import Link from "next/link";

<Link href="/about">About</Link>
<Link href="/blog" prefetch={true}>Blog</Link>
<Link href={{ pathname: "/post", query: { id: 1 } }}>
  Post
</Link>

next/link performs client-side navigation without a reload. Automatic prefetch of visible pages. Accepts a string or an object with pathname and query. Replaces <a> for internal routes.

Shared Component
// components/Card.tsx
export function Card({ title, text }: {
  title: string;
  text: string;
}) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <p>{text}</p>
    </div>
  );
}

Components in components/ are reusable. Without a directive, they are Server Components by default. Add "use client" if you need interactivity (hooks, events).

next/script
import Script from "next/script";

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

next/script optimizes loading of external scripts. strategy: beforeInteractive, afterInteractive (default), lazyOnload. Avoids blocking rendering.

next/font
import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
});

// layout.tsx:
<body className={inter.variable}>

next/font optimizes fonts automatically (zero layout shift). Downloads at build time and serves locally. variable creates a CSS variable to use in Tailwind or custom CSS.

Icon and Manifest
app/
  icon.png        # automatic favicon
  apple-icon.png  # iOS home screen
  manifest.ts     # PWA manifest

// manifest.ts:
export default {
  name: "My App",
  theme_color: "#000",
};

Convention files: icon.png generates a favicon automatically. manifest.ts configures the PWA. No need for manual tags in the <head> — Next.js injects everything.

Data Fetching


10 cards
Server-Side Fetch
// app/page.tsx (Server Component)
export default async function Page() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();

  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

Server Components can be async and use fetch directly. Data is fetched on the server — never exposed to the client. No need for useEffect or state.

generateStaticParams
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

generateStaticParams() pre-generates dynamic routes at build time. It returns an array of params. Each object generates a static page. Without it, dynamic routes are SSR by default.

Loading with Skeleton
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="skeleton">
      <div className="h-4 w-3/4 animate-pulse" />
      <div className="h-4 w-1/2 animate-pulse" />
    </div>
  );
}

loading.tsx is automatically wrapped in Suspense. Shows a skeleton/placeholder during data fetching. Each section can have its own loading for granularity.

SSG (Static)
// Generated at build time (static)
export const dynamic = "force-static";

export default async function Page() {
  const data = await fetch(url);
  return <p>{data.title}</p>;
}

force-static generates the page at build time (SSG). The HTML is served from a CDN with no computation. Ideal for content that does not change. By default, pages without dynamic data are already static.

revalidatePath
"use server";
import { revalidatePath } from "next/cache";

export async function publish() {
  await db.save(post);
  revalidatePath("/blog");
  revalidatePath("/");
}

revalidatePath() invalidates the cache for a path manually. The next visit regenerates the page. Use it in Server Actions after mutations to guarantee fresh data.

unstable_cache
import { unstable_cache } from "next/cache";

const getCachedPosts = unstable_cache(
  async () => db.posts.findMany(),
  ["posts"],
  { revalidate: 3600, tags: ["posts"] }
);

const posts = await getCachedPosts();

unstable_cache caches any async function (not just fetch). Accepts a key, revalidate and tags. Useful for DB queries or heavy computations that do not need an HTTP fetch.

ISR (Revalidation)
// Revalidate every 60 seconds
export const revalidate = 60;

// Or per fetch:
const res = await fetch(url, {
  next: { revalidate: 3600 }
});

revalidate enables ISR (Incremental Static Regeneration). The page is static but regenerates in the background every N seconds. Combines SSG performance with fresh data.

revalidateTag
// Fetch with tag:
fetch(url, { next: { tags: ["posts"] } })

// Invalidate by tag:
import { revalidateTag } from "next/cache";
revalidateTag("posts");

tags group related fetches. revalidateTag() invalidates all fetches with that tag at once. More granular than revalidatePath when multiple pages use the same data.

Fetch Cache
// Indefinite cache (default in production)
fetch(url, { cache: "force-cache" })

// No cache (always fresh)
fetch(url, { cache: "no-store" })

// Time-based revalidation
fetch(url, { next: { revalidate: 60 } })

force-cache stores the response indefinitely. no-store always fetches fresh data (equivalent to SSR). revalidate caches with time-based expiration.

Streaming with Suspense
import { Suspense } from "react";

export default function Page() {
  return (
    <div>
      <h1>Blog</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <PostList />
      </Suspense>
    </div>
  );
}

Suspense enables streaming: it sends the HTML shell immediately and the content when ready. The fallback shows while loading. Improves TTFB and perceived UX.

Routing and Navigation


9 cards
Dynamic Route
// app/blog/[slug]/page.tsx
export default function Post({
  params,
}: { params: { slug: string } }) {
  return <h1>{params.slug}</h1>;
}

Square brackets [slug] create dynamic segments. The value is available in params.slug. /blog/hello-worldparams.slug = "hello-world".

redirect
import { redirect } from "next/navigation";

export default async function Page() {
  const user = await getUser();
  if (!user) {
    redirect("/login");
  }
  return <p>Hello, {user.name}</p>;
}

redirect() redirects on the server (HTTP 307). It throws an exception internally — no return needed after it. Use to protect routes or redirect after actions.

Rewrites and Redirects
// next.config.js
module.exports = {
  async rewrites() {
    return [{ source: "/api/:path*",
      destination: "https://api.external.com/:path*" }];
  },
  async redirects() {
    return [{ source: "/old",
      destination: "/new", permanent: true }];
  },
};

rewrites proxies requests without changing the URL (transparent). redirects sends HTTP 301/308. Configured in next.config.js. Useful for migrations and external APIs.

Catch-all Routes
// app/docs/[...slug]/page.tsx
// /docs/a/b/c → params.slug = ["a", "b", "c"]

// Optional catch-all:
// app/docs/[[...slug]]/page.tsx
// /docs → params.slug = undefined

[...slug] captures multiple segments the an array. [[...slug]] (double brackets) is optional — it also matches the route with no segments.

notFound()
import { notFound } from "next/navigation";

export default async function Post({ params }) {
  const post = await getPost(params.slug);
  if (!post) {
    notFound();
  }
  return <h1>{post.title}</h1>;
}

notFound() renders the nearest not-found.tsx. It throws an internal exception (like redirect). Use it when a resource does not exist in the DB to correctly return 404.

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

export function Button() {
  const router = useRouter();

  return (
    <button onClick={() => router.push("/login")}>
      Sign in
    </button>
  );
}

useRouter() enables programmatic navigation in Client Components. push() adds to the history. replace() replaces. back() goes back. refresh() re-runs the load.

Link with Prefetch
import Link from "next/link";

<Link href="/dashboard" prefetch={true}>
  Dashboard
</Link>

<Link href="/heavy" prefetch={false}>
  Heavy page
</Link>

prefetch loads the page in the background when the link is visible. By default it is automatic for static links. prefetch={false} disables it for heavy or authenticated pages.

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

export function Filter() {
  const pathname = usePathname();   // "/blog"
  const searchParams = useSearchParams();
  const page = searchParams.get("page"); // "2"
}

usePathname() returns the current path. useSearchParams() gives access to query params. Both are reactive — they update when AS URL changes. Client Components only.

Dynamic Route Handlers
// app/api/posts/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const post = await getPost(params.id);
  return Response.json(post);
}

Route handlers in dynamic folders receive params the the second argument. request is the native Request object. Supports all exported HTTP verbs.

API e Server Actions


10 cards
GET Route Handler
// app/api/users/route.ts
export async function GET() {
  const users = await db.users.findMany();
  return Response.json(users);
}

route.ts creates an API endpoint. Export functions named after verbs: GET, POST, PUT, DELETE. Response.json() returns JSON with the correct headers.

Use Server Action in Form
import { createPost } from "./actions";

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

action={serverAction} wires the form directly. Works without JavaScript (progressive enhancement). The form does an automatic POST and the action processes on the server.

Streaming Responses
export async function GET() {
  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue("Hello ");
      setTimeout(() => {
        controller.enqueue("World!");
        controller.close();
      }, 1000);
    },
  });

  return new Response(stream);
}

Route handlers support ReadableStream for streaming responses. Useful for SSE, AI responses or progressive data. The client receives chunks the they are generated.

POST Route Handler
// app/api/users/route.ts
export async function POST(request: Request) {
  const body = await request.json();
  const user = await db.users.create(body);
  return Response.json(user, { status: 201 });
}

request.json() reads the JSON body. request.formData() for forms. The second argument of Response.json() sets custom status and headers.

useFormStatus and useActionState
"use client";
import { useFormStatus } from "react-dom";
import { useActionState } from "react";

function Submit() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>
    {pending ? "Sending..." : "Send"}
  </button>;
}

useFormStatus() gives the form state (pending, data). useActionState() manages the action return value (success/error). Both improve UX during submissions.

CORS and API Middleware
export async function OPTIONS() {
  return new Response(null, {
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST",
      "Access-Control-Allow-Headers": "Content-Type",
    },
  });
}

OPTIONS responds to CORS preflight requests. Set Access-Control-* headers to allow external origins. Alternative: configure CORS in the global middleware.ts.

Query Params and Headers
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const page = searchParams.get("page");

  const auth = request.headers.get("Authorization");
  return Response.json({ page, auth });
}

new URL(request.url) gives access to searchParams. request.headers.get() reads headers. Both use native web APIs (not Next.js specific).

cookies and headers
import { cookies, headers } from "next/headers";

// Read cookie:
const token = cookies().get("token")?.value;

// Set cookie:
cookies().set("theme", "dark", { path: "/" });

// Read header:
const ua = headers().get("user-agent");

cookies() and headers() access the request on the server. They work in Server Components, Route Handlers and Server Actions. cookies().set() sets cookies on the response.

Basic Server Action
"use server";

// app/actions.ts
export async function createPost(formData: FormData) {
  const title = formData.get("title") the string;
  await db.posts.create({ title });
}

"use server" at the top marks the file the Server Actions. Functions run only on the server. They receive FormData or serializable arguments. They replace API endpoints for mutations.

revalidatePath in Action
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function create(formData: FormData) {
  await db.posts.create({ title: formData.get("title") });
  revalidatePath("/blog");
  redirect("/blog");
}

After a mutation, revalidatePath() invalidates the cache and redirect() navigates. The complete pattern: save → invalidate → redirect. The user sees fresh data immediately.

Rendering


9 cards
Server Component (Default)
// app/page.tsx (no directive = Server)
export default async function Page() {
  const data = await fetch(url);
  return <div>{data.title}</div>;
}

By default, all components are Server Components. They can be async, access the DB and use secrets. Zero JavaScript sent to the client — rendered on the server.

dynamic import
import dynamic from "next/dynamic";

const Map = dynamic(() => import("./Map"), {
  ssr: false,
  loading: () => <p>Loading map...</p>,
});

dynamic() does code-splitting and lazy loading. ssr: false disables server rendering (for libs that use window/document). loading shows a fallback.

Hydration and Errors
"use client";
import { useEffect, useState } from "react";

export function Time() {
  const [time, setTime] = useState("");
  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
  }, []);
  return <p>{time}</p>;
}

Hydration mismatch happens when the server HTML differs from the client render. Values like Date.now() or Math.random() cause errors. Use useEffect for client-only values.

Client Component
"use client";

import { useState } from "react";

export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

"use client" at the top marks it the a Client Component. Required for hooks (useState, useEffect), events (onClick) and browser APIs. Runs on the client and server (hydration).

Suspense for Streaming
import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <h1>Dashboard</h1>
      <Suspense fallback={<Skeleton />}>
        <SalesChart />
      </Suspense>
    </>
  );
}

Suspense sends the available HTML immediately and streams the rest. Each Suspense is independent — multiple can resolve in parallel. Improves TTFB significantly.

Composing Server + Client
// app/page.tsx (Server)
import Counter from "./Counter";

export default async function Page() {
  const data = await fetch(url);
  return (
    <div>
      <h1>{data.title}</h1>
      <Counter initial={data.value} />
    </div>
  );
}

Recommended pattern: the Server fetches data and passes it the props to Client Components. Keeps heavy logic on the server. The Client only receives serializable data and handles interactivity.

Static vs Dynamic Rendering
// Force static:
export const dynamic = "force-static";

// Force dynamic (SSR):
export const dynamic = "force-dynamic";

// Runtime:
export const runtime = "edge";

dynamic controls when the page is generated. force-static = build time. force-dynamic = every request. runtime = "edge" runs on the edge (Vercel Edge, Cloudflare Workers).

children the Composition
"use client";
// Modal.tsx
export function Modal({ children }: {
  children: React.ReactNode
}) {
  const [open, setOpen] = useState(true);
  return open ? <div>{children}</div> : null;
}

// Server can pass children:
<Modal><ServerContent /></Modal>

children allows Server Components inside Client Components. The content is rendered on the server and "projected" into the client. Keep the Server tree the high up the possible.

Partial Prerendering (PPR)
// next.config.js
experimental: { ppr: true }

// app/page.tsx
import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <Header />  {/* static */}
      <Suspense fallback={<Skeleton />}>
        <Profile />  {/* dynamic */}
      </Suspense>
    </>
  );
}

PPR (experimental) combines static and dynamic on the same page. The shell is served from a CDN instantly. Dynamic parts inside Suspense are streamed from the server.

Advanced and Deploy


10 cards
Middleware
// middleware.ts (root or src/)
import { NextResponse } from "next/server";

export function middleware(request: Request) {
  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*"],
};

middleware.ts intercepts requests before they reach the route. matcher limits where it runs. Use it for auth, redirects, A/B testing, geolocation. Runs on the edge (fast).

Deploy to Vercel
npm i -g vercel
vercel          # preview deploy
vercel --prod   # production deploy

# Or: push to GitHub
# Vercel detects it and deploys automatically

Vercel is the native Next.js platform. Automatic deploy on every push. Preview deployments per PR. Edge functions, ISR and image optimization included. Zero configuration.

Testing (Jest + RTL)
// __tests__/page.test.tsx
import { render, screen } from "@testing-library/react";
import Home from "@/app/page";

test("shows title", () => {
  render(<Home />);
  expect(screen.getByText("Welcome")).toBeTruthy();
});

@testing-library/react renders components in tests. render() mounts in the virtual DOM. screen.getByText() finds elements. For Server Components, use jest-environment-jsdom.

Edge Runtime
// app/api/geo/route.ts
export const runtime = "edge";

export async function GET(request: Request) {
  const geo = request.geo;
  return Response.json({
    country: geo?.country,
    city: geo?.city,
  });
}

runtime = "edge" runs in edge locations (closer to the user). No access to Node.js APIs (fs, net). Ideal for fast responses: geo, auth, redirects, A/B tests.

Docker (standalone)
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/public ./public
CMD ["node", "server.js"]

output: "standalone" generates a minimal Node server. The multi-stage Dockerfile copies only what is needed. Final image ~50MB. Ideal for VPS, Kubernetes, AWS ECS.

Performance and Core Web Vitals
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });

// Monitor:
export function reportWebVitals(metric) {
  console.log(metric.name, metric.value);
}

// next build shows:
// Route  Size  First Load JS

Next.js optimizes automatically: code-splitting, image optimization, font optimization. next build shows size per route. reportWebVitals monitors LCP, FID, CLS in production.

Dynamic Imports and Lazy
import dynamic from "next/dynamic";

// Without SSR (client only):
const Editor = dynamic(() => import("./Editor"), {
  ssr: false,
});

// With loading:
const Chart = dynamic(() => import("./Chart"), {
  loading: () => <p>Loading...</p>,
});

dynamic() splits the bundle and loads on demand. ssr: false for libs that need window/document. Reduces initial JavaScript — improves First Contentful Paint.

Static Export
// next.config.js
module.exports = {
  output: "export",
};

// Build:
npm run build
// Generates out/ folder with static HTML

output: "export" generates a 100% static site (no Node server). HTML/CSS/JS in the out/ folder. No SSR, API routes or middleware. Ideal for GitHub Pages, S3, Netlify.

Advanced next.config
module.exports = {
  output: "standalone",
  experimental: {
    serverActions: { bodySizeLimit: "2mb" },
  },
  headers: async () => [{
    source: "/(.*)",
    headers: [{ key: "X-Frame-Options", value: "DENY" }],
  }],
};

output: "standalone" generates a minimal build for Docker. headers adds global security headers. experimental enables experimental features. Configure according to deploy needs.

Instrumentation and Logging
// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./sentry.server.config");
  }
  if (process.env.NEXT_RUNTIME === "edge") {
    await import("./sentry.edge.config");
  }
}

instrumentation.ts runs once at server startup. Ideal for initializing Sentry, OpenTelemetry, or DB connections. NEXT_RUNTIME distinguishes Node.js from Edge.

Styling and Assets


9 cards
CSS Modules
// Card.module.css
.card { padding: 1rem; }
.title { font-size: 1.5rem; }

// Card.tsx
import styles from "./Card.module.css";

<div className={styles.card}>
  <h2 className={styles.title}>{title}</h2>
</div>

CSS Modules scope styles per component automatically. Class names are unique (hash). No global conflicts. .module.css files are supported natively.

Advanced next/image
import Image from "next/image";

<Image
  src="https://example.com/photo.jpg"
  fill
  sizes="(max-width: 768px) 100vw, 50vw"
  style={{ objectFit: "cover" }}
  placeholder="blur"
  blurDataURL="data:image/..."
/>

fill for responsive images (the parent needs position: relative). sizes optimizes the srcset. placeholder="blur" shows a blur-up while loading. External domains must be in images.domains.

CSS-in-JS (styled-components)
// lib/registry.tsx
"use client";
import { ServerStyleSheet } from "styled-components";

// next.config.js:
compiler: { styledComponents: true }

// Component:
const Title = styled.h1`
  color: tomato;
  font-size: 2rem;
`;

Next.js supports styled-components with compiler.styledComponents: true. Requires a registry for SSR. Alternatives: Emotion, Vanilla Extract. Tailwind is the most popular option.

Tailwind CSS
// globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

// Component:
<div className="p-4 rounded-lg shadow-md">
  <h2 className="text-xl font-bold">Title</h2>
</div>

Tailwind CSS comes the an option in create-next-app. Inline utility classes. tailwind.config.ts customizes the theme. Automatic purge at build time — only used CSS is included.

Static Files
public/
  logo.png
  favicon.ico
  robots.txt

// Usage:
<img src="/logo.png" />
// Served at: https://site.com/logo.png

The public/ folder serves files at the site root. No processing — copied the-is. For images, prefer next/image (optimization). Ideal for robots.txt, favicons, SVGs.

Global CSS
// app/globals.css
* { box-sizing: border-box; }
body { margin: 0; font-family: sans-serif; }

// Import in the root layout:
// app/layout.tsx
import "./globals.css";

Global CSS can only be imported in app/layout.tsx (root). It applies to all pages. For per-component styles, use CSS Modules or Tailwind. Do not import global CSS in sub-components.

SVG the Component
// next.config.js
module.exports = {
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      use: ["@svgr/webpack"],
    });
    return config;
  },
};

// Usage:
import Logo from "./logo.svg";
<Logo width={48} />

@svgr/webpack lets you import SVGs as React components. Accepts props like width, fill. Alternative: import os URL with next/image.

Styled JSX
export function Card() {
  return (
    <div>
      <p>Hello</p>
      <style jsx>{`
        p { color: blue; font-size: 16px; }
      `}</style>
    </div>
  );
}

styled-jsx is included with Next.js. CSS inside <style jsx> is scoped to the component. Supports global for non-scoped styles. A lightweight alternative to CSS Modules.

Dark Mode with Tailwind
// tailwind.config.ts
darkMode: "class",

// layout.tsx:
<html className="dark">

// Component:
<div className="bg-white dark:bg-gray-900">
  <p className="text-black dark:text-white">Hello</p>
</div>

darkMode: "class" enables dark mode via a class on <html>. The dark: prefix applies styles in dark mode. Combine with next-themes for a toggle with persistence.