Next.js

Referensi Next.js untuk pekerjaan harian: App Router, layout, routing, server/client component, data fetching, cache, route handler, server action, environment, metadata, dan deployment.

00

Project dan Perintah Dasar

Membuat project, menjalankan dev server, build, dan memeriksa kualitas kode.

Membuat project baru

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

Script umum

npm
npm run dev              # development server
npm run build            # production build
npm run start            # jalankan hasil build
npm run lint             # lint Next.js/ESLint

Struktur App Router minimal

tree
app/
  layout.tsx             # root layout wajib
  page.tsx               # route /
  globals.css
  dashboard/
    page.tsx             # route /dashboard
    loading.tsx          # loading UI segmen
    error.tsx            # error boundary client
    not-found.tsx        # UI 404 segmen
components/
lib/
01

Routing App Router

File dan folder di dalam app/ menentukan URL, layout, loading state, dan error boundary.

Route statis dan dinamis

routes
app/page.tsx                         # /
app/about/page.tsx                   # /about
app/blog/[slug]/page.tsx             # /blog/hello
app/docs/[...parts]/page.tsx         # /docs/a/b/c
app/shop/[[...filters]]/page.tsx     # /shop atau /shop/a/b

Membaca params dan searchParams

page.tsx
type Props = {
  params: { slug: string };
  searchParams: { q?: string; page?: string };
};

export default function Page({ params, searchParams }: Props) {
  return (
    <main>
      <h1>{params.slug}</h1>
      <p>Query: {searchParams.q ?? "none"}</p>
    </main>
  );
}

Navigasi client

navigation
"use client";

import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";

export function Nav() {
  const router = useRouter();
  const pathname = usePathname();
  const params = useSearchParams();

  return (
    <>
      <Link href="/dashboard">Dashboard</Link>
      <button onClick={() => router.push("/login")}>Login</button>
      <span>{pathname} ?q={params.get("q")}</span>
    </>
  );
}
02

Server dan Client Component

Default di App Router adalah Server Component. Pakai Client Component hanya saat perlu state, effect, event handler, atau browser API.

Server Component default

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

  return (
    <main>
      <h1>{data.title}</h1>
    </main>
  );
}

Client Component

client
"use client";

import { useState } from "react";

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

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Pola komposisi server ke client

composition
// Server Component
import { Counter } from "@/components/counter";

export default async function Page() {
  const user = await getUser();
  return <Counter initialName={user.name} />;
}
03

Data Fetching dan Cache

Pahami default cache supaya halaman tidak menampilkan data basi atau justru terlalu sering refetch.

fetch dengan cache control

fetch
await fetch("https://api.example.com/posts"); 
// default dapat di-cache pada render statis

await fetch("https://api.example.com/posts", { cache: "no-store" });
// selalu dinamis, cocok untuk data user/request-specific

await fetch("https://api.example.com/posts", {
  next: { revalidate: 60 },
});
// cache dengan revalidate tiap 60 detik

Memaksa static atau dynamic route

route config
export const dynamic = "force-static";
export const revalidate = 3600;

// atau:
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";

Revalidate manual

cache
"use server";

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

export async function refreshPosts() {
  revalidatePath("/posts");
  revalidateTag("posts");
}
04

Route Handler dan API

Gunakan route.ts untuk endpoint HTTP di App Router.

GET dan POST endpoint

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

export async function GET() {
  return NextResponse.json({ ok: true });
}

export async function POST(req: NextRequest) {
  const body = await req.json();
  return NextResponse.json({ received: body }, { status: 201 });
}

Membaca params route handler

dynamic api
type Context = {
  params: { id: string };
};

export async function GET(_req: Request, { params }: Context) {
  return Response.json({ id: params.id });
}

// file: app/api/users/[id]/route.ts

Cookie dan header

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

export async function GET() {
  const token = cookies().get("token")?.value;
  const userAgent = headers().get("user-agent");

  return Response.json({ token: Boolean(token), userAgent });
}
05

Server Actions dan Form

Server Actions menjalankan mutation di server dan cocok untuk form sederhana sampai workflow CRUD.

Action di file server

actions.ts
"use server";

import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
  const title = String(formData.get("title") ?? "");
  await db.post.create({ data: { title } });
  revalidatePath("/posts");
}

Form memakai Server Action

form
import { createPost } from "./actions";

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

Client submit state

useFormStatus
"use client";

import { useFormStatus } from "react-dom";

export function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
}
06

Environment dan Config

Pisahkan variable server-only dan public. Jangan expose secret dengan prefix NEXT_PUBLIC_.

File environment

.env
DATABASE_URL="postgres://user:pass@localhost:5432/app"
OPENAI_API_KEY="sk-..."
NEXT_PUBLIC_APP_URL="http://localhost:3000"

Membaca env

env
const databaseUrl = process.env.DATABASE_URL;        // server-only
const appUrl = process.env.NEXT_PUBLIC_APP_URL;      // client-safe jika bukan secret

if (!databaseUrl) {
  throw new Error("DATABASE_URL is required");
}

next.config.mjs

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

export default nextConfig;
07

Metadata, Image, dan Font

API bawaan untuk SEO, Open Graph, optimisasi image, dan font.

Metadata statis

metadata
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Dashboard",
  description: "Operational dashboard",
  openGraph: {
    title: "Dashboard",
    description: "Operational dashboard",
  },
};

Metadata dinamis

generate
import type { Metadata } from "next";

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

Image dan font

assets
import Image from "next/image";
import { GeistSans } from "geist/font/sans";

export default function Page() {
  return <Image src="/hero.png" alt="Hero" width={1200} height={630} priority />;
}

// className={GeistSans.className}
08

Middleware dan Auth Gate

Middleware berjalan sebelum request masuk route. Cocok untuk redirect ringan, bukan query database berat.

Middleware redirect login

middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(req: NextRequest) {
  const token = req.cookies.get("token")?.value;
  const isPrivate = req.nextUrl.pathname.startsWith("/dashboard");

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

  return NextResponse.next();
}

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

Proteksi di Server Component

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

export default async function DashboardPage() {
  const user = await getCurrentUser();

  if (!user) {
    redirect("/login");
  }

  return <main>Welcome {user.name}</main>;
}
09

Loading, Error, dan 404

Special files membuat state route lebih rapi tanpa wiring manual.

loading.tsx

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

error.tsx

error
"use client";

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

notFound helper

404
import { notFound } from "next/navigation";

export default async function Page({ params }: { params: { id: string } }) {
  const item = await getItem(params.id);

  if (!item) {
    notFound();
  }

  return <main>{item.title}</main>;
}
10

Build, Deploy, dan Debug

Perintah untuk memeriksa output produksi dan masalah umum saat deployment.

Build lokal seperti production

production
npm run build
npm run start

# cek bundle dan route hasil build
NEXT_TELEMETRY_DISABLED=1 npm run build

Vercel CLI

vercel
npm i -g vercel
vercel dev
vercel pull
vercel env pull .env.local
vercel deploy
vercel deploy --prod

Dockerfile standalone

next.config
const nextConfig = {
  output: "standalone",
};

export default nextConfig;