Claude Code dan Next.js full-stack: panduan praktis App Router
Panduan Claude Code dan Next.js App Router untuk batas server/client, Server Actions, API, validasi, auth, dan review.
Claude Code bisa membuat fitur full-stack Next.js dengan cepat. Namun kecepatan itu hanya berguna jika batas App Router jelas sejak awal. Jika prompt terlalu umum, logika server bisa masuk ke komponen browser, terlalu banyak file berubah menjadi Client Component, atau JSON yang belum divalidasi langsung disimpan.
Panduan ini memakai contoh dashboard tugas untuk pengguna yang sudah login. Kita akan membahas struktur App Router, kapan memakai Server Components dan Client Components, Route Handlers, Server Actions, validasi, environment variables, batas auth, dan prompt review arsitektur untuk Claude Code.
Untuk rujukan resmi, buka Next.js App Router docs, Server and Client Components, Route Handlers, Mutating Data, dan Backend for Frontend guide. Untuk alur kerja agen, lihat Claude Code common workflows.
Tentukan batas terlebih dahulu
Di App Router, bagian sulit bukan menghafal nama fitur, tetapi memahami di mana kode berjalan. Server Component dirender di server. Client Component berjalan di browser dan dipakai untuk state, event, atau API browser. Server Action adalah fungsi mutasi di server, biasanya dipanggil dari form. Route Handler adalah endpoint HTTP untuk JSON API, webhook, atau client eksternal. BFF berarti Backend for Frontend, yaitu lapisan backend tipis untuk kebutuhan satu UI.
| Area | Kapan dipakai | Boleh berisi | Instruksi untuk Claude Code |
|---|---|---|---|
| Server Component | Render awal, baca DB, halaman SEO | Akses DB, auth check, private API | Tetap server-first kecuali ada interaksi nyata |
| Client Component | Form, modal, tab, optimistic UI | useState, useActionState, event handler | Jangan import secret, DB client, atau server-only |
| Server Action | Create, update, delete dari UI | Validasi, auth, mutasi, revalidation | Jangan dipakai sebagai public API |
| Route Handler | External API, webhook, mobile client | JSON, status HTTP, signature check | Wajib validasi input dan auth |
Berikan tabel ini ke Claude Code sebelum implementasi. Aturan utamanya sederhana: jangan expose secret di Client Component. Hanya environment variable dengan prefix NEXT_PUBLIC_ yang memang untuk browser.
flowchart TD
Browser[Form browser] --> Client[Client Component]
Client --> Action[Server Action]
External[Layanan eksternal] --> Route[Route Handler]
Page[Server Component] --> Auth[Batas auth]
Action --> Auth
Route --> Auth
Auth --> Data[DB atau logika server]
Data --> Page
Struktur proyek
Minta Claude Code bekerja dengan struktur file yang eksplisit. Di App Router, filesystem adalah model routing; tanpa struktur, logika mudah tersebar.
src/
app/
dashboard/
tasks/
page.tsx
new/
page.tsx
actions.ts
api/
tasks/
route.ts
components/
task-create-form.tsx
lib/
auth.ts
env.ts
tasks.ts
page.tsx merender tampilan awal, task-create-form.tsx menangani interaksi browser, actions.ts memproses mutasi dari UI, route.ts menyediakan HTTP API, dan lib menyimpan logika server.
Pisahkan logika server-only
Storage berikut sengaja memakai memori agar contoh mudah dicoba. Untuk produksi, ganti dengan Prisma, Drizzle, Supabase, atau data layer internal. Bagian pentingnya adalah server-only, supaya modul ini tidak salah di-import oleh Client Component.
// src/lib/tasks.ts
import "server-only";
export type TaskPriority = "low" | "normal" | "high";
export type Task = {
id: string;
ownerId: string;
title: string;
priority: TaskPriority;
dueDate: string | null;
createdAt: string;
};
const tasks: Task[] = [];
export async function listTasks(options: {
ownerId: string;
priority?: TaskPriority;
}) {
return tasks
.filter((task) => task.ownerId === options.ownerId)
.filter((task) => !options.priority || task.priority === options.priority)
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
export async function createTask(input: {
ownerId: string;
title: string;
priority: TaskPriority;
dueDate?: string | null;
}) {
const task: Task = {
id: crypto.randomUUID(),
ownerId: input.ownerId,
title: input.title,
priority: input.priority,
dueDate: input.dueDate ?? null,
createdAt: new Date().toISOString(),
};
tasks.push(task);
return task;
}
Batas auth juga tetap di server. Contoh ini menganggap cookie demo_user_id sebagai pengguna login. Pada produk nyata, ganti dengan Auth.js, Clerk, atau identity provider internal.
// src/lib/auth.ts
import "server-only";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export type CurrentUser = {
id: string;
name: string;
role: "member" | "admin";
};
export async function getCurrentUser(): Promise<CurrentUser | null> {
const cookieStore = await cookies();
const userId = cookieStore.get("demo_user_id")?.value;
if (!userId) {
return null;
}
return {
id: userId,
name: "Demo User",
role: "member",
};
}
export async function requireUser() {
const user = await getCurrentUser();
if (!user) {
redirect("/login");
}
return user;
}
export async function requireApiUser() {
return getCurrentUser();
}
Environment variables sebaiknya dipusatkan dan divalidasi. Jangan import file ini dari Client Component.
// src/lib/env.ts
import "server-only";
import { z } from "zod";
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
APP_SECRET: z.string().min(32),
});
export const env = EnvSchema.parse(process.env);
Halaman dengan Server Component
Daftar tugas cocok menjadi Server Component. Data authenticated dibaca di server, bundle browser lebih kecil, dan kita menghindari loading client yang tidak perlu.
// src/app/dashboard/tasks/page.tsx
import Link from "next/link";
import { requireUser } from "@/lib/auth";
import { listTasks } from "@/lib/tasks";
export default async function TasksPage() {
const user = await requireUser();
const tasks = await listTasks({ ownerId: user.id });
return (
<main className="mx-auto max-w-3xl space-y-6 p-6">
<div className="flex items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold">Tasks</h1>
<p className="text-sm text-gray-600">Work owned by {user.name}</p>
</div>
<Link className="rounded bg-black px-4 py-2 text-white" href="/dashboard/tasks/new">
New task
</Link>
</div>
<ul className="divide-y rounded border">
{tasks.map((task) => (
<li className="flex items-center justify-between p-4" key={task.id}>
<div>
<p className="font-medium">{task.title}</p>
<p className="text-sm text-gray-500">
Priority: {task.priority} / Due: {task.dueDate ?? "none"}
</p>
</div>
</li>
))}
</ul>
</main>
);
}
Instruksikan Claude Code untuk tidak menambahkan use client ke file ini kecuali ada kebutuhan interaksi browser yang nyata.
Mutasi dengan Server Action
Server Actions cocok untuk mutasi form. Auth, validasi, penyimpanan, dan revalidation sebaiknya berada dalam satu fungsi. Zod dipakai untuk memvalidasi input sebelum menyentuh data layer.
// src/app/dashboard/tasks/new/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { requireUser } from "@/lib/auth";
import { createTask } from "@/lib/tasks";
const CreateTaskSchema = z.object({
title: z.string().trim().min(1, "Title is required").max(80),
priority: z.enum(["low", "normal", "high"]),
dueDate: z
.string()
.trim()
.optional()
.transform((value) => (value ? value : null)),
});
export type TaskFormState = {
ok: boolean;
message?: string;
fieldErrors?: Record<string, string[]>;
};
export async function createTaskAction(
previousState: TaskFormState,
formData: FormData
): Promise<TaskFormState> {
const user = await requireUser();
const parsed = CreateTaskSchema.safeParse({
title: formData.get("title"),
priority: formData.get("priority"),
dueDate: formData.get("dueDate"),
});
if (!parsed.success) {
return {
ok: false,
fieldErrors: parsed.error.flatten().fieldErrors,
message: "Please check the form fields.",
};
}
await createTask({
ownerId: user.id,
...parsed.data,
});
revalidatePath("/dashboard/tasks");
return {
ok: true,
message: "Task created.",
};
}
Client Component hanya memegang state browser dan rendering form. Jangan masukkan DB, auth secret, atau modul env di sini.
// src/components/task-create-form.tsx
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import {
createTaskAction,
type TaskFormState,
} from "@/app/dashboard/tasks/new/actions";
const initialState: TaskFormState = {
ok: false,
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
className="rounded bg-black px-4 py-2 text-white disabled:opacity-50"
disabled={pending}
type="submit"
>
{pending ? "Creating..." : "Create"}
</button>
);
}
export function TaskCreateForm() {
const [state, formAction] = useActionState(createTaskAction, initialState);
return (
<form action={formAction} className="space-y-4 rounded border p-4">
<div>
<label className="block text-sm font-medium" htmlFor="title">
Title
</label>
<input
className="mt-1 w-full rounded border px-3 py-2"
id="title"
name="title"
type="text"
/>
{state.fieldErrors?.title?.map((error) => (
<p className="mt-1 text-sm text-red-600" key={error}>
{error}
</p>
))}
</div>
<div>
<label className="block text-sm font-medium" htmlFor="priority">
Priority
</label>
<select className="mt-1 w-full rounded border px-3 py-2" id="priority" name="priority">
<option value="normal">Normal</option>
<option value="high">High</option>
<option value="low">Low</option>
</select>
</div>
<div>
<label className="block text-sm font-medium" htmlFor="dueDate">
Due date
</label>
<input className="mt-1 w-full rounded border px-3 py-2" id="dueDate" name="dueDate" type="date" />
</div>
{state.message ? <p className="text-sm text-gray-700">{state.message}</p> : null}
<SubmitButton />
</form>
);
}
// src/app/dashboard/tasks/new/page.tsx
import { TaskCreateForm } from "@/components/task-create-form";
export default function NewTaskPage() {
return (
<main className="mx-auto max-w-xl p-6">
<h1 className="mb-4 text-2xl font-bold">Create a task</h1>
<TaskCreateForm />
</main>
);
}
API dengan Route Handler
Gunakan Route Handler untuk integrasi eksternal, webhook, mobile client, atau JSON API. Server Actions praktis untuk mutasi UI, tetapi bukan pengganti kontrak HTTP yang jelas.
// src/app/api/tasks/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { requireApiUser } from "@/lib/auth";
import { createTask, listTasks } from "@/lib/tasks";
export const runtime = "nodejs";
const PrioritySchema = z.enum(["low", "normal", "high"]);
const CreateTaskApiSchema = z.object({
title: z.string().trim().min(1).max(80),
priority: PrioritySchema.default("normal"),
dueDate: z.string().date().nullable().optional(),
});
export async function GET(request: NextRequest) {
const user = await requireApiUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const priority = request.nextUrl.searchParams.get("priority");
const parsedPriority = priority ? PrioritySchema.safeParse(priority) : null;
if (parsedPriority && !parsedPriority.success) {
return NextResponse.json({ error: "Invalid priority" }, { status: 400 });
}
const tasks = await listTasks({
ownerId: user.id,
priority: parsedPriority?.data,
});
return NextResponse.json({ data: tasks });
}
export async function POST(request: NextRequest) {
const user = await requireApiUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json().catch(() => null);
const parsed = CreateTaskApiSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid request body", details: parsed.error.flatten().fieldErrors },
{ status: 400 }
);
}
const task = await createTask({
ownerId: user.id,
...parsed.data,
});
return NextResponse.json({ data: task }, { status: 201 });
}
Use case praktis
Use case pertama adalah dashboard request internal. Daftar tetap memakai Server Component, pembuatan request memakai Server Action, dan notifikasi ke Slack atau tool lain memakai Route Handler.
Use case kedua adalah halaman pengaturan SaaS. Billing, undangan tim, dan pembuatan API key sebaiknya memiliki UI ringan, sementara pengecekan permission dilakukan di Server Action.
Use case ketiga adalah CMS atau admin produk. Daftar awal dirender di server, sedangkan upload gambar, webhook, dan publish notification dipisah ke Route Handler. Jika ada perubahan database, baca juga panduan automasi migrasi DB.
Use case keempat adalah arsitektur BFF. Daripada browser memanggil banyak API pihak ketiga secara langsung, gabungkan di balik Next.js Route Handler agar secret tetap di server dan kontrak UI stabil.
Kesalahan yang sering terjadi
Risiko terbesar adalah membiarkan Claude Code mengaburkan batas. Periksa apakah Client Component meng-import helper DB, apakah initial data loading dipindah tanpa alasan ke useEffect, apakah Server Action dipakai seperti public API, atau apakah Route Handler menyimpan request.json() tanpa validasi.
Auth juga harus ditegakkan di server. Menyembunyikan tombol bukan authorization. Server Actions dan Route Handlers perlu mengecek user, ownership, dan role. Untuk detail, lihat panduan implementasi autentikasi.
Hindari prompt samar seperti “rapikan saja”. Perubahan kecil di App Router bisa memengaruhi cache, revalidation, form state, dan auth boundary. Selalu berikan scope file, hal yang dilarang, dan perintah verifikasi.
Prompt review untuk Claude Code
Setelah implementasi, minta review dulu sebelum mengizinkan edit.
You are reviewing a Next.js App Router full-stack change.
Scope:
- src/app/dashboard/tasks
- src/app/api/tasks/route.ts
- src/components/task-create-form.tsx
- src/lib/auth.ts
- src/lib/tasks.ts
- src/lib/env.ts
Check:
1. No secrets, DB clients, or server-only modules are imported by Client Components.
2. Server Components are not converted to Client Components without a real interaction need.
3. Server Actions validate input, check auth, mutate data, and revalidate the affected path.
4. Route Handlers return correct HTTP status codes and validate JSON bodies.
5. Auth is enforced on the server, not only hidden in the UI.
6. Tests or manual verification steps are listed for each risk.
Do not edit files yet. Return findings by severity with file paths and concrete fixes.
Setelah itu, perbaiki satu temuan setiap kali. Minimal jalankan lint, typecheck, test relevan, submit form manual, dan request API tanpa login yang harus menghasilkan 401. Untuk perencanaan lebih luas, lihat panduan strategi testing.
Training dan template ClaudeCodeLab
Untuk proyek solo, struktur dan prompt dalam artikel ini cukup untuk mulai. Untuk tim, ubah aturan boundary, checklist review, aturan auth, dan policy environment menjadi template yang bisa dipakai ulang.
ClaudeCodeLab menyediakan Claude Code training and templates serta consultation for team adoption. Gunakan jika ingin menstandarkan admin screen Next.js, CMS, pengaturan SaaS, dan review loop sesuai aturan repositori tim.
Ringkasan
Claude Code bisa mempercepat pengembangan full-stack Next.js, tetapi hanya jika batas server dan browser ditentukan dulu. Gunakan Server Components untuk initial rendering, Client Components untuk interaksi, Server Actions untuk mutasi UI, dan Route Handlers untuk API eksternal.
Setelah mencoba workflow dalam artikel ini, manfaat terbesar bukan jumlah kode yang dihasilkan, melainkan berkurangnya noise saat review. Ketika tabel batas dan struktur file diberikan lebih dulu ke Claude Code, perubahan berikutnya kebanyakan hanya copy UI dan penyesuaian kecil. Saat prompt awal terlalu samar, logika form dan API bercampur, lalu review memakan waktu lebih lama daripada implementasi.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.