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.
Project dan Perintah Dasar
Membuat project, menjalankan dev server, build, dan memeriksa kualitas kode.
Membuat project baru
npx create-next-app@latest my-app cd my-app npm run dev
Script umum
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
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/Routing App Router
File dan folder di dalam app/ menentukan URL, layout, loading state, dan error boundary.
Route statis dan dinamis
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
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
"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>
</>
);
}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
export default async function Page() {
const data = await getDataFromDatabase();
return (
<main>
<h1>{data.title}</h1>
</main>
);
}Client Component
"use client" hanya perlu di file entry Client Component. Child component yang diimpor dari sana ikut masuk client bundle."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
// Server Component
import { Counter } from "@/components/counter";
export default async function Page() {
const user = await getUser();
return <Counter initialName={user.name} />;
}Data Fetching dan Cache
Pahami default cache supaya halaman tidak menampilkan data basi atau justru terlalu sering refetch.
fetch dengan cache control
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 detikMemaksa static atau dynamic route
export const dynamic = "force-static"; export const revalidate = 3600; // atau: export const dynamic = "force-dynamic"; export const fetchCache = "force-no-store";
Revalidate manual
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function refreshPosts() {
revalidatePath("/posts");
revalidateTag("posts");
}Route Handler dan API
Gunakan route.ts untuk endpoint HTTP di App Router.
GET dan POST endpoint
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
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.tsCookie dan header
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 });
}Server Actions dan Form
Server Actions menjalankan mutation di server dan cocok untuk form sederhana sampai workflow CRUD.
Action di file server
"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
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
"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
}Environment dan Config
Pisahkan variable server-only dan public. Jangan expose secret dengan prefix NEXT_PUBLIC_.
File environment
DATABASE_URL="postgres://user:pass@localhost:5432/app" OPENAI_API_KEY="sk-..." NEXT_PUBLIC_APP_URL="http://localhost:3000"
Membaca env
NEXT_PUBLIC_ masuk ke browser bundle. Jangan gunakan prefix itu untuk token, password, atau private key.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
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [{ protocol: "https", hostname: "images.example.com" }],
},
};
export default nextConfig;Metadata, Image, dan Font
API bawaan untuk SEO, Open Graph, optimisasi image, dan font.
Metadata statis
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Dashboard",
description: "Operational dashboard",
openGraph: {
title: "Dashboard",
description: "Operational dashboard",
},
};Metadata dinamis
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
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}Middleware dan Auth Gate
Middleware berjalan sebelum request masuk route. Cocok untuk redirect ringan, bukan query database berat.
Middleware redirect login
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
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user) {
redirect("/login");
}
return <main>Welcome {user.name}</main>;
}Loading, Error, dan 404
Special files membuat state route lebih rapi tanpa wiring manual.
loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}error.tsx
"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
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>;
}Build, Deploy, dan Debug
Perintah untuk memeriksa output produksi dan masalah umum saat deployment.
Build lokal seperti production
npm run build npm run start # cek bundle dan route hasil build NEXT_TELEMETRY_DISABLED=1 npm run build
Vercel CLI
npm i -g vercel vercel dev vercel pull vercel env pull .env.local vercel deploy vercel deploy --prod
Dockerfile standalone
standalone di next.config.mjs lalu copy folder .next/standalone.const nextConfig = {
output: "standalone",
};
export default nextConfig;