Proyek: Blog + API
Tugas akhir: blog lengkap dengan API publik dan auth sederhana.
Proyek akhir: blog full-stack
Proyek terbesar di track ini: routing dinamis, Server Components, metadata, API publik, proteksi auth, Server Action bervalidasi, dan sitemap dalam satu aplikasi. Ikuti urutan di bawah, commit setiap langkah selesai.
Langkah 1: fondasi data
Mulai dari satu sumber data agar semua fitur konsisten. Untuk proyek ini cukup in-memory (array di module), database bisa menyusul nanti:
// lib/posts.ts
export type Post = {
slug: string;
judul: string;
isi: string;
tanggal: string;
};
export const posts: Post[] = [
{
slug: "belajar-nextjs",
judul: "Belajar Next.js dari Nol",
isi: "Next.js adalah framework React...",
tanggal: "2026-10-01",
},
// ... tambah 4-5 artikel dummy
];
export function getPost(slug: string) {
return posts.find((p) => p.slug === slug);
}Langkah 2: daftar artikel dan halaman detail
// app/page.tsx
import Link from "next/link";
import { posts } from "@/lib/posts";
export default function Beranda() {
return (
<main>
<h1>Blog Saya</h1>
{posts.map((p) => (
<article key={p.slug}>
<Link href={`/blog/${p.slug}`}><h2>{p.judul}</h2></Link>
<time>{p.tanggal}</time>
</article>
))}
</main>
);
}// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { posts, getPost } from "@/lib/posts";
export function generateStaticParams() {
return posts.map((p) => ({ slug: p.slug }));
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const post = getPost(slug);
if (!post) return { title: "Tidak ditemukan" };
return { title: post.judul, description: post.isi.slice(0, 150) };
}
export default async function Detail({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = getPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.judul}</h1>
<time>{post.tanggal}</time>
<p>{post.isi}</p>
</article>
);
}generateStaticParams membuat semua artikel di-build statis saat deploy: cepat dan murah. generateMetadata memberi tiap artikel judul dan deskripsi unik untuk SEO.
Langkah 3: API publik
// app/api/posts/route.ts
import { NextResponse } from "next/server";
import { posts } from "@/lib/posts";
export async function GET() {
return NextResponse.json(posts);
}Satu file, dan blog-mu kini punya API publik yang bisa dikonsumsi aplikasi lain. Uji dengan curl http://localhost:3000/api/posts dan pastikan JSON-nya valid.
Langkah 4: form tulis artikel dengan Server Action
// app/tulis/page.tsx
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { posts } from "@/lib/posts";
const schema = z.object({
judul: z.string().min(5, "Judul minimal 5 karakter"),
isi: z.string().min(20, "Isi minimal 20 karakter"),
});
async function tambahArtikel(formData: FormData) {
"use server";
const parsed = schema.safeParse({
judul: formData.get("judul"),
isi: formData.get("isi"),
});
if (!parsed.success) return; // di app nyata: kembalikan error ke form
const slug = parsed.data.judul.toLowerCase().replace(/\s+/g, "-");
posts.push({ slug, tanggal: new Date().toISOString().slice(0, 10), ...parsed.data });
revalidatePath("/");
redirect(`/blog/${slug}`);
}
export default function Tulis() {
return (
<form action={tambahArtikel}>
<input name="judul" placeholder="Judul artikel" required />
<textarea name="isi" placeholder="Isi artikel" required />
<button type="submit">Terbitkan</button>
</form>
);
}Validasi Zod dengan safeParse memastikan data kotor tidak pernah masuk. revalidatePath("/") memaksa halaman beranda me-render ulang agar artikel baru langsung muncul.
Langkah 5: proteksi halaman /tulis
Halaman tulis hanya untuk admin. Versi sederhana memakai cookie admin=1 yang kamu set manual:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
if (req.nextUrl.pathname.startsWith("/tulis") && req.cookies.get("admin")?.value !== "1") {
return NextResponse.redirect(new URL("/", req.url));
}
return NextResponse.next();
}
export const config = { matcher: ["/tulis/:path*"] };Set cookie manual lewat DevTools (Application > Cookies) dengan nama admin dan nilai 1 untuk menguji. Tanpa cookie itu, akses ke /tulis mental ke beranda.
Langkah 6: sitemap otomatis
// app/sitemap.ts
import type { MetadataRoute } from "next";
import { posts } from "@/lib/posts";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: "https://blogku.com", lastModified: new Date() },
...posts.map((p) => ({
url: `https://blogku.com/blog/${p.slug}`,
lastModified: new Date(p.tanggal),
})),
];
}Urutan pengerjaan yang disarankan
- Langkah 1-2: routing dan tampilan (fondasi).
- Langkah 3: API publik (cepat, memberi rasa pencapaian).
- Langkah 4: form + validasi (bagian tersulit, kerjakan saat fokus).
- Langkah 5-6: proteksi dan sitemap (penyelesaian).
Commit tiap langkah. Mentok lebih dari 30 menit? Catat yang sudah dicoba, lanjut ke langkah berikut.
Tantangan
Pecah tugas
Sebelum ngoding, pecah 6 fitur wajib di atas menjadi checklist dengan perkiraan file untuk masing-masing. Urutkan dari yang paling mudah.
Tugas
Blog Full-Stack
Bangun blog: daftar + detail [slug] (SSG), generateMetadata dinamis, GET /api/posts, halaman /tulis terproteksi cookie admin, form Server Action + Zod + revalidatePath, sitemap.ts. Tulis kode lengkap file-file utamanya.
Kriteria penilaian:
- Dynamic route [slug] dengan generateStaticParams
- generateMetadata dinamis per artikel
- Route Handler GET /api/posts me-return JSON valid
- Proteksi /tulis via middleware/cookie
- Server Action + validasi Zod + revalidatePath
AI tutor akan memeriksa bug, kesalahan syntax, dan memberi saran perbaikan.
Kuis Bab
Uji pemahamanmu: API, Middleware & Deploy
Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".
1.File apa yang membuat API endpoint di App Router?
2.Kapan memakai Route Handler dibanding Server Action?
3.Di mana middleware berjalan?
4.Apa bahaya prefix NEXT_PUBLIC_ pada secret?
5.Kenapa proteksi auth memakai dua lapis (middleware + page)?