September 27, 2026

Next.js (Full-Stack).

Next.js 16 sebagai framework full-stack React: App Router, Server & Client Components, Route Handlers, Server Actions, proxy.ts, Cache Components, data layer, testing, dan contoh yang sudah di-build.

Next.js biasanya dikenal sebagai framework frontend React, tapi sejak App Router ia juga framework full-stack: komponen bisa berjalan di server dan langsung membaca database, form bisa memanggil fungsi server tanpa membuat endpoint, dan API tetap bisa dibuat lewat Route Handlers. Aku memakai Next.js di Fanny Web Profile dan My Anime List .

Versi yang dibahas: Next.js 16 (terbaru di npm per September 2026: 16.3.x, dengan React 19), butuh Node.js ≥ 20.9. Catatan ini fokus ke sisi server; dasar React ada di catatan React .


1. Filosofi

  • React-first, server-first — di App Router, komponen adalah Server Component secara default. JavaScript ke browser hanya dikirim untuk komponen yang ditandai 'use client'.
  • File-system routing — folder = segmen URL; file khusus (page, layout, route, loading, error) menentukan perilaku.
  • Rendering & caching terpadu — satu framework mengurus SSR, static prerender, streaming, dan cache.
  • Tidak opinionated soal backend — tidak ada ORM, validator, atau DI bawaan. Next.js menyediakan tempat menjalankan kode server; pilihan data layer bebas.

2. Struktur Project

npx create-next-app@latest my-app
cd my-app
npm run dev   # http://localhost:3000

Struktur full-stack yang umum:

my-app/
├── app/
│   ├── layout.tsx              # root layout (<html>, <body>)
│   ├── page.tsx                # /
│   ├── loading.tsx             # skeleton (Suspense boundary)
│   ├── error.tsx               # error boundary
│   ├── actions.ts              # Server Actions ('use server')
│   ├── (marketing)/            # route group — tidak masuk URL
│   │   └── about/page.tsx      # /about
│   ├── posts/
│   │   ├── page.tsx            # /posts
│   │   └── [slug]/page.tsx     # /posts/:slug
│   └── api/
│       └── posts/
│           ├── route.ts        # GET/POST /api/posts
│           └── [id]/route.ts   # GET /api/posts/:id
├── components/                 # komponen UI (server & client)
├── lib/
│   ├── db.ts                   # Prisma/Drizzle client
│   └── posts.ts                # data access layer ('server-only')
├── public/
├── proxy.ts                    # dulu middleware.ts
├── next.config.ts
├── instrumentation.ts          # OpenTelemetry (opsional)
└── package.json
FileFungsi
page.tsxMembuat route bisa diakses
layout.tsxUI bersama, tidak re-render saat navigasi
route.tsEndpoint API (tidak boleh satu folder dengan page.tsx)
loading.tsx / error.tsx / not-found.tsxSuspense / error boundary / 404
[slug], [...slug], [[...slug]]Segmen dinamis, catch-all, opsional catch-all
(group), _folderRoute group, folder privat (tidak di-routing)

3. Server Components vs Client Components

Server Component (default)Client Component ('use client')
Jalan diServer (build atau request)Server (SSR awal) + browser
Akses DB / secret / fileYaTidak
useState, useEffect, event handlerTidakYa
Ukuran bundle JSTidak menambahMenambah
async componentYaTidak
// app/posts/page.tsx — Server Component
import { getPosts } from '@/lib/posts'
import LikeButton from '@/components/like-button' // client component

export default async function PostsPage() {
  const posts = await getPosts() // query langsung, tanpa fetch ke API sendiri
  return posts.map((p) => (
    <article key={p.id}>
      <h2>{p.title}</h2>
      <LikeButton postId={p.id} />
    </article>
  ))
}

Aturan praktis: jadikan komponen interaktif sekecil mungkin sebagai client component, sisanya biarkan di server.


4. Routing Backend: Route Handlers

Route Handler memakai Web API standar Request/Response. Satu fungsi per HTTP method.

// app/api/posts/[id]/route.ts
export async function GET(_req: Request, ctx: RouteContext<'/api/posts/[id]'>) {
  const { id } = await ctx.params // params adalah Promise sejak v15
  return Response.json({ id })
}

RouteContext adalah helper tipe global yang di-generate saat next dev/next build/next typegen. Method yang didukung: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS (OPTIONS dibuat otomatis jika tidak didefinisikan).

Kapan pakai Route Handler, kapan Server Action?

KebutuhanPilihan
Mutasi dari form/komponen di app sendiriServer Action
API publik, mobile app, pihak ketigaRoute Handler
Webhook (Stripe, GitHub, dll.)Route Handler
RSS, sitemap dinamis, file downloadRoute Handler
Membaca data untuk render halamanLangsung di Server Component

5. Server Actions (Server Functions)

Fungsi async di file bertanda 'use server' bisa dipanggil dari form atau client component. Next.js membuat endpoint POST internal secara otomatis.

// app/actions.ts
'use server'

import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { createPost } from '@/lib/posts'

const schema = z.object({ title: z.string().trim().min(3) })

export async function addPost(formData: FormData) {
  const parsed = schema.safeParse({ title: formData.get('title') })
  if (!parsed.success) return
  await createPost(parsed.data.title)
  revalidatePath('/') // buang cache halaman agar data baru tampil
}

Penting: Server Action tetap endpoint publik. Selalu validasi input dan cek autentikasi/otorisasi di dalam action — dokumentasi Next.js secara eksplisit menyarankan tidak mengandalkan proxy.ts saja untuk proteksi.


6. Middleware → proxy.ts

Di Next.js 16, file middleware.ts di-deprecate dan diganti nama menjadi proxy.ts, dan kini default memakai Node.js runtime. Fungsinya: redirect, rewrite, set header/cookie sebelum route dirender.

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  if (!request.cookies.has('session')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/dashboard/:path*'],
}

Migrasi dari file lama:

npx @next/codemod@canary middleware-to-proxy .

Tidak ada rantai middleware per route seperti Express. Untuk logika per endpoint, buat helper biasa (mis. withAuth(handler)) atau cek di awal handler/action.


7. Validasi

Tidak ada validator bawaan. Zod paling umum karena bisa dipakai bersama di server dan client. Untuk form dengan pesan error, gabungkan Server Action dengan hook React useActionState untuk mengembalikan state error ke UI.


8. Caching (Cache Components)

Next.js 16 memperkenalkan Cache Components (opt-in) dengan directive 'use cache':

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}
export default nextConfig
// lib/posts.ts
import { cacheLife, cacheTag } from 'next/cache'

export async function getPosts() {
  'use cache'
  cacheLife('hours')
  cacheTag('posts')
  return db.query.posts.findMany()
}

Dengan mode ini, bagian statis/ter-cache masuk ke static shell dan bagian dinamis di-stream lewat <Suspense> (Partial Prerendering). Tanpa cacheComponents, Next.js memakai model cache lama (export const revalidate = 60, dll.).


9. Data Layer

Next.js tidak punya ORM. Pola yang direkomendasikan: Data Access Layer — semua akses DB dikumpulkan di lib/ dan ditandai import 'server-only' supaya build gagal jika tidak sengaja di-import dari client component.

PilihanCatatan
PrismaPaling populer; perhatikan batas koneksi di serverless (pakai pooler)
DrizzleRingan, cocok untuk serverless/edge
KyselyQuery builder type-safe
Supabase / Firebase SDKBaaS
Backend terpisahNext.js hanya sebagai BFF yang memanggil API Fastify/Nest/Adonis

Hindari memanggil Route Handler milik sendiri dari Server Component (fetch('/api/...')) — panggil fungsi data layer-nya langsung.


10. Testing

LevelTool
Unit (fungsi, client component)Vitest atau Jest + React Testing Library
Route HandlerPanggil fungsi GET/POST langsung dengan new Request(...)
proxy.tsnext/experimental/testing/server (unstable_doesProxyMatch)
Server Component async & E2EPlaywright atau Cypress

Contoh test Route Handler dengan Vitest:

import { describe, it, expect } from 'vitest'
import { POST } from '@/app/api/posts/route'

describe('POST /api/posts', () => {
  it('menolak title pendek', async () => {
    const req = new Request('http://localhost/api/posts', {
      method: 'POST',
      body: JSON.stringify({ title: 'x' }),
    })
    const res = await POST(req)
    expect(res.status).toBe(422)
  })
})

Async Server Component belum didukung penuh oleh tool unit test, jadi dokumentasi Next.js menyarankan E2E untuk kasus ini.


11. Performa

Next.js tidak bisa dibandingkan apple-to-apple dengan Express/Fastify dalam req/s, karena pekerjaannya berbeda: me-render React, men-stream HTML + RSC payload, dan mengelola cache. Tidak ada angka Next.js di fastify/benchmarks , dan aku tidak mencantumkan angka karangan. Yang lebih relevan:

  • Halaman statis/ter-cache dilayani dari CDN atau disk — sangat cepat, hampir tanpa kerja server.
  • Server Components mengurangi JavaScript di browser → loading lebih cepat di sisi pengguna.
  • Route Handler punya overhead lebih besar daripada Fastify murni (routing Next + lapisan Web Request/Response). Untuk API throughput tinggi, backend terpisah biasanya lebih tepat.
  • Serverless cold start dan koneksi database jadi perhatian jika di-deploy ke Vercel/sejenisnya.
  • Build dan dev server memakai Turbopack secara default.

12. Ekosistem & Kematangan

AspekPenilaian
PengembangVercel, bekerja sama erat dengan tim React
PopularitasFramework React paling populer
DeploymentVercel (paling mulus), Node server, Docker, static export, adapter platform lain
Stabilitas APIBerubah cukup cepat (params jadi Promise di v15, middleware → proxy di v16, model cache baru) — baca upgrade guide tiap versi mayor
Dokumentasinextjs.org/docs — sangat lengkap

13. Contoh Minimal (Sudah Di-build)

Contoh ini aku build dan jalankan dengan Next.js 16.3.6: halaman list + form Server Action, plus REST API.

npx create-next-app@latest next-demo --yes   # default: TypeScript, App Router, alias @/*
cd next-demo
npm i zod server-only

lib/posts.ts:

import 'server-only'

export type Post = { id: number; title: string }

// "database" in-memory untuk demo — ganti dengan Prisma/Drizzle
const posts: Post[] = [{ id: 1, title: 'Halo Next.js' }]

export async function getPosts() {
  return [...posts].reverse()
}

export async function getPost(id: number) {
  return posts.find((p) => p.id === id) ?? null
}

export async function createPost(title: string) {
  const post = { id: posts.length + 1, title }
  posts.push(post)
  return post
}

app/actions.ts: sama seperti di bagian 5.

app/page.tsx:

import { getPosts } from '@/lib/posts'
import { addPost } from './actions'

export default async function Home() {
  const posts = await getPosts()

  return (
    <main>
      <h1>Posts</h1>
      <form action={addPost}>
        <input name="title" placeholder="Judul post" required />
        <button type="submit">Tambah</button>
      </form>
      <ul>
        {posts.map((p) => (
          <li key={p.id}>{p.title}</li>
        ))}
      </ul>
    </main>
  )
}

app/api/posts/route.ts:

import { z } from 'zod'
import { getPosts, createPost } from '@/lib/posts'

export async function GET() {
  return Response.json(await getPosts())
}

const schema = z.object({ title: z.string().trim().min(3) })

export async function POST(request: Request) {
  const parsed = schema.safeParse(await request.json())
  if (!parsed.success) {
    return Response.json({ errors: parsed.error.issues }, { status: 422 })
  }
  return Response.json(await createPost(parsed.data.title), { status: 201 })
}

app/api/posts/[id]/route.ts:

import { getPost } from '@/lib/posts'

export async function GET(_req: Request, ctx: RouteContext<'/api/posts/[id]'>) {
  const { id } = await ctx.params
  const post = await getPost(Number(id))
  if (!post) return Response.json({ message: 'Not found' }, { status: 404 })
  return Response.json(post)
}
npm run build && npm start

curl localhost:3000/api/posts
curl -X POST localhost:3000/api/posts -H 'Content-Type: application/json' -d '{"title":"Kedua"}'
curl localhost:3000/api/posts/2

Perhatikan output next build: / ditandai ○ (Static) karena tidak membaca data request, sedangkan /api/posts ƒ (Dynamic). Itu sebabnya action memanggil revalidatePath('/') — tanpa itu, halaman statis tidak akan menampilkan post baru.


14. Kapan Memilih Next.js?

  • Produk yang UI-nya React dan butuh SEO, SSR/SSG, dan backend ringan dalam satu repo.
  • Tim frontend yang ingin menulis logika server tanpa mengelola service terpisah.
  • Situs konten, dashboard, e-commerce, landing page, portofolio.

Kurang cocok sebagai satu-satunya backend untuk API publik berat, job/queue jangka panjang, atau WebSocket — di situ pasangkan dengan Fastify , NestJS , atau AdonisJS . Lihat Perbandingan Framework Node.js .


Referensi

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments