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
| File | Fungsi |
|---|---|
page.tsx | Membuat route bisa diakses |
layout.tsx | UI bersama, tidak re-render saat navigasi |
route.ts | Endpoint API (tidak boleh satu folder dengan page.tsx) |
loading.tsx / error.tsx / not-found.tsx | Suspense / error boundary / 404 |
[slug], [...slug], [[...slug]] | Segmen dinamis, catch-all, opsional catch-all |
(group), _folder | Route group, folder privat (tidak di-routing) |
3. Server Components vs Client Components
| Server Component (default) | Client Component ('use client') | |
|---|---|---|
| Jalan di | Server (build atau request) | Server (SSR awal) + browser |
| Akses DB / secret / file | Ya | Tidak |
useState, useEffect, event handler | Tidak | Ya |
| Ukuran bundle JS | Tidak menambah | Menambah |
async component | Ya | Tidak |
// 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?
| Kebutuhan | Pilihan |
|---|---|
| Mutasi dari form/komponen di app sendiri | Server Action |
| API publik, mobile app, pihak ketiga | Route Handler |
| Webhook (Stripe, GitHub, dll.) | Route Handler |
| RSS, sitemap dinamis, file download | Route Handler |
| Membaca data untuk render halaman | Langsung 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.
| Pilihan | Catatan |
|---|---|
| Prisma | Paling populer; perhatikan batas koneksi di serverless (pakai pooler) |
| Drizzle | Ringan, cocok untuk serverless/edge |
| Kysely | Query builder type-safe |
| Supabase / Firebase SDK | BaaS |
| Backend terpisah | Next.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
| Level | Tool |
|---|---|
| Unit (fungsi, client component) | Vitest atau Jest + React Testing Library |
| Route Handler | Panggil fungsi GET/POST langsung dengan new Request(...) |
proxy.ts | next/experimental/testing/server (unstable_doesProxyMatch) |
| Server Component async & E2E | Playwright 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
| Aspek | Penilaian |
|---|---|
| Pengembang | Vercel, bekerja sama erat dengan tim React |
| Popularitas | Framework React paling populer |
| Deployment | Vercel (paling mulus), Node server, Docker, static export, adapter platform lain |
| Stabilitas API | Berubah cukup cepat (params jadi Promise di v15, middleware → proxy di v16, model cache baru) — baca upgrade guide tiap versi mayor |
| Dokumentasi | nextjs.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.