Web Development

Next.js untuk Pemula: React Framework Full-Stack

Tutorial lengkap belajar Next.js dari nol β€” SSR vs CSR, App Router, Server Components, API Routes, data fetching, dan deployment ke Vercel dengan contoh kode praktis

1. Pengenalan Next.js

Next.js adalah framework React yang dikembangkan oleh Vercel yang memungkinkan Anda membangun aplikasi web full-stack dengan mudah. Next.js menambahkan fitur-fitur penting di atas React seperti server-side rendering (SSR), static site generation (SSG), routing otomatis, API routes, dan optimasi gambar β€” semuanya out of the box.

Sejak diluncurkan pada tahun 2016, Next.js telah menjadi framework React paling populer untuk produksi. Perusahaan besar seperti Netflix, TikTok, Twitch, Hulu, dan Nike menggunakan Next.js untuk membangun website mereka.

Mengapa Memilih Next.js?

Fitur Penjelasan
Server-Side RenderingHalaman di-render di server, menghasilkan HTML yang langsung dikirim ke browser β€” lebih cepat dan SEO-friendly
Static Site GenerationHalaman bisa di-build saat build time untuk performa maksimal
File-Based RoutingCukup buat file di folder, routing otomatis dibuat β€” tidak perlu konfigurasi manual
API RoutesBuat API backend langsung di dalam proyek Next.js tanpa server terpisah
Optimasi OtomatisImage optimization, font optimization, script optimization β€” semua built-in
Full-StackFrontend dan backend dalam satu proyek yang sama

Next.js vs React vs Framework Lain

Aspek Next.js React (SPA) Remix
RenderingSSR, SSG, ISR, CSRCSR sajaSSR
RoutingFile-based (App Router)React Router (manual)File-based
APIAPI Routes / Route HandlersPerlu backend terpisahLoader/Action
SEO🟒 Sangat baik🟑 Perlu SSR setup🟒 Sangat baik
Belajar🟑 Sedang🟒 Mudah🟑 Sedang
DeploymentVercel, Docker, Self-hostStatic hostingVarious
Diagram: Posisi Next.js dalam Ekosistem Web
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              EKOSISTEM REACT / WEB MODERN              β”‚
β”‚                                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                  Next.js                        β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚App Router β”‚ β”‚ API Routeβ”‚ β”‚Server Componentβ”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚ SSR/SSG   β”‚ β”‚Image Opt β”‚ β”‚ Middleware     β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                        β–²                              β”‚
β”‚                        β”‚ built on                      β”‚
β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚              β”‚     React.js       β”‚                   β”‚
β”‚              β”‚  (Library UI)      β”‚                   β”‚
β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Setup Proyek Next.js

Untuk membuat proyek Next.js baru, jalankan perintah berikut di terminal:

Bash
# Buat proyek Next.js baru dengan App Router
npx create-next-app@latest belajar-nextjs

# Pilihan yang disarankan saat setup:
# βœ” Would you like to use TypeScript? β†’ Yes
# βœ” Would you like to use ESLint? β†’ Yes
# βœ” Would you like to use Tailwind CSS? β†’ Yes
# βœ” Would you like to use `src/` directory? β†’ Yes
# βœ” Would you like to use App Router? β†’ Yes
# βœ” Would you like to customize the default import alias? β†’ No

# Masuk ke direktori proyek
cd belajar-nextjs

# Jalankan development server
npm run dev

# Output:
#   β–² Next.js 15.x
#   - Local:        http://localhost:3000
#   - Environments: .env.local

Struktur Proyek Next.js

File Structure
belajar-nextjs/
β”œβ”€β”€ node_modules/
β”œβ”€β”€ public/                 ← File statis (gambar, favicon)
β”‚   β”œβ”€β”€ next.svg
β”‚   └── vercel.svg
β”œβ”€β”€ src/
β”‚   └── app/                ← App Router (utama)
β”‚       β”œβ”€β”€ layout.tsx      ← Root layout (wajib)
β”‚       β”œβ”€β”€ page.tsx        ← Halaman utama (/)
β”‚       β”œβ”€β”€ globals.css     ← CSS global
β”‚       β”œβ”€β”€ about/
β”‚       β”‚   └── page.tsx    ← Halaman /about
β”‚       β”œβ”€β”€ blog/
β”‚       β”‚   β”œβ”€β”€ page.tsx    ← Halaman /blog
β”‚       β”‚   └── [slug]/
β”‚       β”‚       └── page.tsx ← Halaman /blog/[slug]
β”‚       └── api/
β”‚           └── hello/
β”‚               └── route.ts ← API endpoint /api/hello
β”œβ”€β”€ .env.local              ← Environment variables
β”œβ”€β”€ next.config.js          ← Konfigurasi Next.js
β”œβ”€β”€ package.json
└── tsconfig.json           ← Konfigurasi TypeScript
πŸ’‘ Tips

Next.js versi terbaru menggunakan App Router secara default (folder app/). Versi lama menggunakan Pages Router (folder pages/). Untuk proyek baru, gunakan App Router karena mendukung fitur-fitur terbaru seperti React Server Components dan streaming.

2. SSR vs CSR: Memahami Rendering

Salah satu alasan utama menggunakan Next.js adalah dukungan berbagai strategi rendering. Memahami perbedaan antara Client-Side Rendering (CSR) dan Server-Side Rendering (SSR) sangat penting untuk membangun aplikasi web yang cepat dan SEO-friendly.

Client-Side Rendering (CSR)

Pada CSR, seluruh aplikasi di-render di browser menggunakan JavaScript. Browser pertama-tama menerima HTML kosong, lalu JavaScript dijalankan untuk mengisi konten. Ini adalah cara kerja default React SPA (Single Page Application).

Penjelasan β€” Alur CSR
# Alur Client-Side Rendering (CSR):
#
# 1. User mengakses halaman
# 2. Server mengirim HTML kosong + bundle JS besar
# 3. Browser mengunduh JS (bisa 1-3 detik)
# 4. JavaScript dijalankan β†’ render konten
# 5. User baru bisa melihat dan berinteraksi
#
# ❌ Masalah:
# - Loading lambat (blank screen sampai JS selesai)
# - SEO buruk (search engine melihat HTML kosong)
# - First Contentful Paint (FCP) lama

# Contoh: React SPA biasa dengan React Router
# β†’ Semua rendering terjadi di browser
# β†’ Data di-fetch dari client setelah halaman dimuat

Server-Side Rendering (SSR)

Pada SSR, halaman di-render di server setiap kali ada request. Server mengirim HTML lengkap yang langsung bisa ditampilkan browser β€” tanpa menunggu JavaScript.

Penjelasan β€” Alur SSR
# Alur Server-Side Rendering (SSR):
#
# 1. User mengakses halaman
# 2. Server meng-fetch data dan render komponen
# 3. Server mengirim HTML lengkap ke browser
# 4. Browser langsung menampilkan konten
# 5. JavaScript "hydration" β†’ interaktif
#
# βœ… Keuntungan:
# - Konten langsung terlihat (FCP cepat)
# - SEO sangat baik (search engine dapat HTML lengkap)
# - Cocok untuk konten dinamis yang sering berubah

# Di Next.js, gunakan fungsi ini untuk SSR:
# export async function generateMetadata() { ... }
# fetch() dengan cache: 'no-store'

Perbandingan Rendering di Next.js

Metode Kapan Render Konten Cocok Untuk
SSR (Dynamic)Setiap requestTerbaruDashboard, halaman user
SSG (Static)Saat buildStatisBlog, landing page
ISRSaat build + revalidateSemi-dinamisE-commerce, berita
CSRDi browserDinamisApp interaktif
Diagram: Perbandingan SSR vs CSR
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    CLIENT-SIDE (CSR)                     β”‚
β”‚                                                         β”‚
β”‚  Browser         Server                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”                               β”‚
β”‚  β”‚      │──────▢│      β”‚  Request                      β”‚
β”‚  β”‚      │◀──────│      β”‚  HTML kosong + JS bundle       β”‚
β”‚  β”‚      β”‚       β””β”€β”€β”€β”€β”€β”€β”˜                               β”‚
β”‚  β”‚ ⏳   β”‚  Download JS... (lambat)                      β”‚
β”‚  β”‚ ⏳   β”‚  Execute JS...                                β”‚
β”‚  β”‚ βœ…   β”‚  Konten muncul (terlambat)                    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”˜                                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                  SERVER-SIDE (SSR)                       β”‚
β”‚                                                         β”‚
β”‚  Browser         Server                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”                               β”‚
β”‚  β”‚      │──────▢│      β”‚  Request                      β”‚
β”‚  β”‚      β”‚       β”‚ ⚑   β”‚  Render di server              β”‚
β”‚  β”‚      β”‚       β”‚ ⚑   β”‚  Fetch data                    β”‚
β”‚  β”‚      │◀──────│      β”‚  HTML lengkap                  β”‚
β”‚  β”‚ βœ…   β”‚       β””β”€β”€β”€β”€β”€β”€β”˜  Konten langsung terlihat!     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”˜                                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Contoh SSR di Next.js App Router

TypeScript β€” SSR Contoh
// src/app/blog/page.tsx
// Secara default, komponen di App Router adalah Server Component
// Jadi fetch() di sini berjalan di server

interface Post {
  id: number;
  title: string;
  body: string;
}

// Komponen ini di-render di server (SSR)
export default async function BlogPage() {
  // Data di-fetch di server β€” tidak dikirim ke browser
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    cache: 'no-store', // SSR: selalu data terbaru
  });
  const posts: Post[] = await response.json();

  return (
    <main>
      <h1>Daftar Blog</h1>
      <p>Total: {posts.length} artikel</p>
      {posts.slice(0, 10).map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </main>
  );
}

Static Site Generation (SSG) dengan ISR

TypeScript β€” SSG + ISR
// src/app/products/page.tsx
// Static generation dengan revalidation setiap 60 detik

export default async function ProductsPage() {
  // next: { revalidate: 60 } = ISR
  // Halaman di-build secara statis, tapi di-regenerate
  // setiap 60 detik jika ada request baru
  const response = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 },
  });
  const products = await response.json();

  return (
    <div>
      <h1>Produk Kami</h1>
      <div className="product-grid">
        {products.map((product: any) => (
          <div key={product.id} className="product-card">
            <h3>{product.name}</h3>
            <p>Rp {product.price.toLocaleString('id-ID')}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

3. App Router dan Struktur Proyek

App Router adalah sistem routing baru di Next.js 13+ yang menggunakan folder dan file khusus untuk mendefinisikan rute. Ini menggantikan Pages Router yang lebih lama dan mendukung React Server Components, streaming, dan fitur modern lainnya.

Konvensi File dalam App Router

File Fungsi
page.tsxMembuat rute yang bisa diakses user
layout.tsxLayout bersama untuk rute dan child-nya
loading.tsxUI loading (suspense boundary)
error.tsxError boundary untuk menangkap error
not-found.tsxHalaman 404 untuk rute tersebut
route.tsAPI endpoint (Route Handler)
template.tsxSeperti layout, tapi re-mount setiap navigasi

Membuat Rute Dasar

File Structure β€” Routing
# File-based routing di App Router:
#
# src/app/page.tsx              β†’ /
# src/app/about/page.tsx        β†’ /about
# src/app/blog/page.tsx         β†’ /blog
# src/app/blog/[slug]/page.tsx  β†’ /blog/apa-saja
# src/app/dashboard/
#   β”œβ”€β”€ layout.tsx               β†’ Layout bersama
#   β”œβ”€β”€ page.tsx                 β†’ /dashboard
#   β”œβ”€β”€ settings/page.tsx        β†’ /dashboard/settings
#   └── profile/page.tsx         β†’ /dashboard/profile
#
# Tidak perlu konfigurasi router!
# Cukup buat folder + file page.tsx

Contoh Root Layout

TypeScript β€” Root Layout
// src/app/layout.tsx (file wajib di App Router)
// Ini membungkus semua halaman dalam aplikasi

import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
  title: 'Aplikasi Next.js Saya',
  description: 'Belajar Next.js dengan mudah',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="id">
      <body>
        <header>
          <nav>
            <a href="/">Beranda</a>
            <a href="/about">Tentang</a>
            <a href="/blog">Blog</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>
          <p>&copy; 2026 Aplikasi Saya</p>
        </footer>
      </body>
    </html>
  );
}

Dynamic Routes

TypeScript β€” Dynamic Route
// src/app/blog/[slug]/page.tsx
// Route dinamis: /blog/nextjs-dasar, /blog/react-hooks, dll.

interface BlogPostProps {
  params: Promise<{ slug: string }>;
}

export default async function BlogPost({ params }: BlogPostProps) {
  const { slug } = await params;

  // Fetch data berdasarkan slug
  const response = await fetch(`https://api.example.com/posts/${slug}`);
  const post = await response.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// Generate metadata dinamis untuk SEO
export async function generateMetadata({ params }: BlogPostProps) {
  const { slug } = await params;
  const response = await fetch(`https://api.example.com/posts/${slug}`);
  const post = await response.json();

  return {
    title: post.title,
    description: post.excerpt,
  };
}
⚠️ Perbedaan Layout vs Template

Layout mempertahankan state saat navigasi antar child routes β€” tidak re-mount. Sedangkan Template re-mount setiap kali user berpindah rute. Gunakan layout untuk sidebar/navigasi yang konsisten, dan template untuk animasi transisi halaman atau efek yang perlu reset state.

4. Server Components vs Client Components

Di Next.js App Router, setiap komponen secara default adalah Server Component β€” artinya komponen tersebut di-render di server dan tidak mengirim JavaScript ke browser. Ini menghasilkan bundle size yang lebih kecil dan performa yang lebih baik.

Namun, jika komponen membutuhkan interaksi pengguna (click, input, dll), browser API, atau React hooks, komponen tersebut harus ditandai sebagai Client Component menggunakan direktif "use client".

Perbandingan Server vs Client Components

Fitur Server Component Client Component
RenderingDi serverDi browser (dengan hydration)
JavaScript ke client❌ Tidakβœ… Ya
useState/useEffect❌ Tidak bisaβœ… Bisa
Akses database/APIβœ… Langsung❌ Perlu fetch()
Event handler❌ Tidak bisaβœ… Bisa
Browser API❌ Tidak bisaβœ… Bisa
Bundle size🟒 0 KB (tidak dikirim)🟑 Sesuai kode

Server Component (Default)

TypeScript β€” Server Component
// app/posts/page.tsx β€” Server Component (default, tanpa "use client")
// Bisa langsung akses database, file system, API rahasia

import { db } from '@/lib/database';

export default async function PostsPage() {
  // Langsung query database! API key tetap aman di server
  const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC');

  return (
    <div>
      <h1>Daftar Postingan</h1>
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
          <time>{new Date(post.created_at).toLocaleDateString('id-ID')}</time>
        </article>
      ))}
    </div>
  );
}

// Komponen ini TIDAK mengirim JavaScript ke browser
// Semua kode di atas hanya berjalan di server
// Database credentials TIDAK pernah sampai ke browser

Client Component

TypeScript β€” Client Component
'use client'; // ← Directive ini WAJIB di baris pertama

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null; // Hindari hydration mismatch

  return (
    <div className="counter">
      <h2>Hitungan: {count}</h2>
      <button onClick={() => setCount(c => c + 1)}>
        Tambah (+1)
      </button>
      <button onClick={() => setCount(c => c - 1)}>
        Kurang (-1)
      </button>
      <button onClick={() => setCount(0)}>
        Reset
      </button>
    </div>
  );
}

Pola Terbaik: Komposisi Server dan Client

TypeScript β€” Komposisi
// components/AddToCartButton.tsx β€” Client Component
'use client';

import { useState } from 'react';

export default function AddToCartButton({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    setLoading(true);
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId }),
    });
    setLoading(false);
    alert('Produk ditambahkan ke keranjang!');
  };

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Menambahkan...' : 'πŸ›’ Tambah ke Keranjang'}
    </button>
  );
}

// app/products/[id]/page.tsx β€” Server Component
import AddToCartButton from '@/components/AddToCartButton';
import { db } from '@/lib/database';

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await db.query('SELECT * FROM products WHERE id = ?', [id]);

  return (
    <div className="product-detail">
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p className="price">Rp {product.price.toLocaleString('id-ID')}</p>
      {/* Client Component di-render di dalam Server Component */}
      <AddToCartButton productId={id} />
    </div>
  );
}
πŸ’‘ Pola Komposisi yang Tepat

Prinsipnya: Server Component di luar, Client Component di dalam. Server Component bisa mengimpor dan merender Client Component sebagai children. Tapi Client Component TIDAK BISA mengimpor Server Component. Jika butuh pola itu, render Server Component sebagai children dari Client Component.

5. API Routes: Backend dalam Next.js

Next.js memungkinkan Anda membuat API endpoint langsung di dalam proyek yang sama menggunakan Route Handlers. Ini sangat berguna untuk form submission, autentikasi, webhook, atau proxy API β€” tanpa perlu server backend terpisah.

Membuat API Route Dasar

TypeScript β€” API Route GET
// src/app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

// GET /api/users
export async function GET(request: NextRequest) {
  // Ambil query parameters
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '10');

  // Di sini Anda bisa fetch dari database
  const users = [
    { id: 1, nama: 'Budi Santoso', email: 'budi@email.com' },
    { id: 2, nama: 'Sari Dewi', email: 'sari@email.com' },
    { id: 3, nama: 'Andi Pratama', email: 'andi@email.com' },
  ];

  return NextResponse.json({
    success: true,
    data: users,
    pagination: { page, limit, total: users.length },
  });
}

POST, PUT, DELETE Methods

TypeScript β€” CRUD API Routes
// src/app/api/users/route.ts (tambahkan method lain)

// POST /api/users β€” Buat user baru
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();

    // Validasi input
    if (!body.nama || !body.email) {
      return NextResponse.json(
        { error: 'Nama dan email wajib diisi' },
        { status: 400 }
      );
    }

    // Simpan ke database (contoh)
    const newUser = {
      id: Date.now(),
      nama: body.nama,
      email: body.email,
    };

    // await db.insert('users', newUser);

    return NextResponse.json(
      { success: true, data: newUser },
      { status: 201 }
    );
  } catch (error) {
    return NextResponse.json(
      { error: 'Gagal membuat user' },
      { status: 500 }
    );
  }
}

// src/app/api/users/[id]/route.ts
// DELETE /api/users/:id β€” Hapus user
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;

  // await db.delete('users', id);

  return NextResponse.json({ success: true, message: `User ${id} dihapus` });
}
Diagram: Arsitektur Full-Stack Next.js
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   NEXT.JS APPLICATION                  β”‚
β”‚                                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚    FRONTEND           β”‚  β”‚   BACKEND           β”‚  β”‚
β”‚  β”‚                       β”‚  β”‚                     β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚ Server Componentβ”‚  β”‚  β”‚  β”‚ API Routes    β”‚  β”‚  β”‚
β”‚  β”‚  β”‚ (SSR/SSG)       β”‚  β”‚  β”‚  β”‚ /api/users    β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚  β”‚ /api/auth     β”‚  β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚  β”‚ /api/products β”‚  β”‚  β”‚
β”‚  β”‚  β”‚ Client Componentβ”‚  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚  β”‚ (Interaktif)    β”‚  β”‚  β”‚         β”‚           β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚         β–Ό           β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚            β”‚                β”‚  β”‚   Database     β”‚  β”‚  β”‚
β”‚            β–Ό                β”‚  β”‚  PostgreSQL    β”‚  β”‚  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚  β”‚  MongoDB       β”‚  β”‚  β”‚
β”‚  β”‚     Browser          β”‚   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚  (User melihat UI)  β”‚   β”‚                     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

6. Data Fetching di Next.js

Next.js memperluas API fetch() bawaan browser untuk memberikan kontrol penuh atas caching dan revalidasi data. Anda bisa melakukan fetching data langsung di Server Components, yang memungkinkan akses database atau API secara aman.

Fetching di Server Component

TypeScript β€” Data Fetching
// 1. Fetch dengan caching (default) β€” data di-cache
async function getProducts() {
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

// 2. Fetch tanpa caching (SSR) β€” data selalu terbaru
async function getDashboard() {
  const res = await fetch('https://api.example.com/dashboard', {
    cache: 'no-store', // Setiap request: data baru dari server
  });
  return res.json();
}

// 3. Fetch dengan ISR (revalidate setiap 60 detik)
async function getArticles() {
  const res = await fetch('https://api.example.com/articles', {
    next: { revalidate: 60 }, // Cache 60 detik, lalu regenerate
  });
  return res.json();
}

// 4. Fetch dengan tag (untuk on-demand revalidation)
async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { tags: ['posts', `post-${slug}`] },
  });
  return res.json();
}

// Penggunaan di komponen
export default async function HomePage() {
  const products = await getProducts();
  const articles = await getArticles();

  return (
    <div>
      <h1>Beranda</h1>
      <section>
        <h2>Produk Terbaru</h2>
        {products.map((p: any) => (
          <div key={p.id}>{p.name} β€” Rp {p.price}</div>
        ))}
      </section>
      <section>
        <h2>Artikel Terbaru</h2>
        {articles.map((a: any) => (
          <article key={a.id}>{a.title}</article>
        ))}
      </section>
    </div>
  );
}

Fetching di Client Component

TypeScript β€” Client Fetching
'use client';

import { useState, useEffect } from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
}

export default function ProductSearch() {
  const [query, setQuery] = useState('');
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(false);

  // Fetch data saat query berubah
  useEffect(() => {
    if (!query.trim()) {
      setProducts([]);
      return;
    }

    const controller = new AbortController();

    async function searchProducts() {
      setLoading(true);
      try {
        const res = await fetch(`/api/products?q=${query}`, {
          signal: controller.signal,
        });
        const data = await res.json();
        setProducts(data);
      } catch (err: any) {
        if (err.name !== 'AbortError') {
          console.error('Gagal mencari produk:', err);
        }
      } finally {
        setLoading(false);
      }
    }

    // Debounce: tunggu 300ms setelah user berhenti mengetik
    const timer = setTimeout(searchProducts, 300);

    return () => {
      clearTimeout(timer);
      controller.abort();
    };
  }, [query]);

  return (
    <div className="search">
      <input
        type="text"
        placeholder="Cari produk..."
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      {loading && <p>Mencari...</p>}
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            {product.name} β€” Rp {product.price.toLocaleString('id-ID')}
          </li>
        ))}
      </ul>
    </div>
  );
}
⚠️ Hindari Waterfall Fetching

Jika Anda memiliki beberapa data yang saling tidak bergantung, jangan fetch secara berurutan (sequential). Gunakan Promise.all() untuk fetch secara paralel. Next.js juga bisa melakukan deduplication β€” jika URL yang sama di-fetch beberapa kali, hanya satu request yang dikirim.

7. Deployment ke Vercel

Vercel adalah platform hosting yang dikembangkan oleh pembuat Next.js. Deployment ke Vercel sangat mudah β€” cukup push ke GitHub dan Vercel otomatis build dan deploy aplikasi Anda.

Cara Deploy ke Vercel

Bash β€” Deploy ke Vercel
# Cara 1: Deploy via Vercel CLI
# Instal Vercel CLI
npm i -g vercel

# Login ke Vercel
vercel login

# Deploy proyek (jalankan di root proyek)
vercel

# Deploy ke production
vercel --prod

# Cara 2: Deploy via GitHub (Disarankan)
# 1. Push kode ke repository GitHub
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/username/nextjs-app.git
git push -u origin main

# 2. Buka vercel.com β†’ Import Project
# 3. Pilih repository GitHub Anda
# 4. Vercel otomatis detect Next.js dan deploy
# 5. Setiap push ke main = auto deploy!

Environment Variables

Bash β€” Environment Variables
# .env.local (jangan commit ke git!)
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
NEXT_PUBLIC_API_URL=https://api.example.com
SECRET_KEY=rahasia-jangan-dibagikan

# Variabel dengan prefix NEXT_PUBLIC_ bisa diakses di browser
# Variabel TANPA prefix hanya bisa diakses di server
# Ini menjaga API key dan secret tetap aman

# Di Vercel Dashboard:
# Project Settings β†’ Environment Variables
# Tambahkan variabel yang sama untuk Production, Preview, Development
Diagram: Alur Deploy Next.js ke Vercel
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              ALUR DEPLOY NEXT.JS KE VERCEL            β”‚
β”‚                                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Local   β”‚     β”‚ GitHub   β”‚     β”‚    Vercel     β”‚  β”‚
β”‚  β”‚ Dev     │────▢│ Push     │────▢│  Auto Build   β”‚  β”‚
β”‚  β”‚         β”‚     β”‚          β”‚     β”‚  Auto Deploy  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                           β”‚          β”‚
β”‚                                    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”   β”‚
β”‚                                    β”‚ Production  β”‚   β”‚
β”‚                                    β”‚ yourapp.    β”‚   β”‚
β”‚                                    β”‚ vercel.app  β”‚   β”‚
β”‚                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                       β”‚
β”‚  Fitur Vercel:                                        β”‚
β”‚  βœ… Auto SSL/HTTPS                                    β”‚
β”‚  βœ… Global CDN                                        β”‚
β”‚  βœ… Preview Deployment (per PR)                       β”‚
β”‚  βœ… Serverless Functions (API Routes)                 β”‚
β”‚  βœ… Edge Functions (Middleware)                        β”‚
β”‚  βœ… Analytics                                         β”‚
β”‚  βœ… Environment Variables                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’‘ Self-Hosting Next.js

Selain Vercel, Next.js juga bisa di-deploy ke platform lain. Gunakan output: 'standalone' di next.config.js untuk menghasilkan build yang bisa dijalankan dengan Docker. Anda juga bisa deploy ke Netlify, AWS, Railway, atau VPS biasa.

8. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Next.js:

Pertanyaan 1: Apa perbedaan utama antara SSR dan CSR?

a) SSR di-render di server dan mengirim HTML lengkap, CSR di-render di browser dengan JavaScript
b) SSR lebih lambat dari CSR
c) CSR lebih SEO-friendly daripada SSR
d) Tidak ada perbedaan, hanya nama lain

Pertanyaan 2: Bagaimana cara membuat Client Component di Next.js App Router?

a) Tambahkan "use client" di baris pertama file
b) Gunakan useEffect di setiap komponen
c) Import dari 'next/client'
d) Semua komponen otomatis menjadi Client Component

Pertanyaan 3: File apa yang WAJIB ada di folder app/ pada Next.js?

a) page.tsx
b) layout.tsx
c) index.tsx
d) app.tsx

Pertanyaan 4: Apa fungsi dari cache: 'no-store' pada fetch() di Next.js?

a) Menyimpan data di local storage browser
b) Meng-cache data selamanya
c) Mengambil data terbaru setiap request (SSR)
d) Menonaktifkan fetch() sepenuhnya

Pertanyaan 5: Apa itu ISR (Incremental Static Regeneration)?

a) Teknik untuk regenerate seluruh website setiap kali deploy
b) Teknik yang menggabungkan SSG dan SSR β€” halaman di-build statis tapi bisa di-regenerate secara periodik
c) Teknik untuk menghapus cache browser secara otomatis
d) Teknik untuk mengkompilasi TypeScript
πŸ” Zoom
100%
🎨 Tema