nextjsproyekfullstackMahir6 mnt baca

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:

ts
// 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

tsx
// 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>
  );
}
tsx
// 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

ts
// 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

tsx
// 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:

ts
// 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

tsx
// 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

  1. Langkah 1-2: routing dan tampilan (fondasi).
  2. Langkah 3: API publik (cepat, memberi rasa pencapaian).
  3. Langkah 4: form + validasi (bagian tersulit, kerjakan saat fokus).
  4. 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)?