Akses database dari server component Next.js memang aman dari mata user. Tapi satu query N+1, satu koneksi tanpa pooling, atau satu row tanpa filter tenant bisa bikin aplikasi SaaS-mu lambat lalu bocor. Kuncinya ada di query optimization, connection pooling, caching strategis, dan authorization ketat di setiap layer data.
Kamu sudah pindah ke server components. Semua query database kini jalan di server. Nggak ada lagi API endpoint yang terekspos. Nggak ada lagi token di browser. Rasanya aman. Rasanya bersih.
Terus suatu hari, customer komplain. Dashboard lambat. Data tetangga tenant muncul di layar. Tagihan database melonjak 3x lipat. Padahal user cuma naik 20%.
Ini skenario nyata yang sering menimpa full-stack developer dan SaaS builder. Server components memang menyembunyikan database dari client. Tapi justru karena itu, banyak developer jadi lengah. Mereka kira “server = aman” lalu mengabaikan praktik akses data yang benar.
Mari kita bedah jebakan paling berbahaya dan cara menghindarinya.
Jebakan 1: N+1 Query di Server yang Nggak Kelihatan
Dulu, N+1 query gampang ketahuan. Kamu lihat network tab, ada puluhan request ke API. Sekarang? Semua terjadi di server. Tanpa alat monitoring yang tepat, N+1 jadi hantu yang bikin lambat tanpa jejak.
Contoh klasik: kamu fetch 100 artikel. Lalu di dalam loop, kamu fetch penulis masing-masing artikel. Hasilnya? 1 query untuk artikel, 100 query untuk penulis. Total 101 query untuk satu halaman.
Di server components, ini sering terjadi saat kamu pakai ORM tanpa eager loading. Atau saat kamu memisahkan data fetching ke beberapa fungsi kecil yang masing-masing buka koneksi sendiri.
Solusinya:
- Gunakan
includeataueager loadingdi Prisma/Drizzle untuk mengambil relasi dalam satu query. - Kalau data berasal dari sumber berbeda, pakai
Promise.all()untuk parallel fetch. - Pasang monitoring query. Tools seperti Prisma Optimize atau pg_stat_statements di PostgreSQL bisa kasih tahu query mana yang paling sering dieksekusi.
// Buruk: N+1
const posts = await db.post.findMany();
for (const post of posts) {
post.author = await db.user.findUnique({ where: { id: post.authorId } });
}
// Baik: Eager loading
const posts = await db.post.findMany({
include: { author: true }
});
Baca juga: 11 Teknik Optimisasi Database Paling Ampuh untuk pendalaman query optimization.
Jebakan 2: Connection Pooling yang Kacau di Lingkungan Serverless
Ini jebakan paling mahal. Server components di Next.js berjalan di environment yang bisa cold start kapan saja. Kalau tiap request membuka koneksi database baru, kamu akan menghantam limit koneksi database dengan sangat cepat.
Bayangkan: satu instance PostgreSQL punya limit 100 koneksi. Satu halaman Next.js dengan 3 server components yang masing-masing fetch data. Kalau tiap komponen buka koneksi sendiri, satu user bisa pakai 3 koneksi. Dengan 50 user bersamaan, 150 koneksi. Database-mu tumbang.
Solusinya:
- Gunakan connection pooler. PgBouncer atau PgCat untuk PostgreSQL. ProxySQL untuk MySQL.
- Untuk deployment di Vercel atau serverless, pertimbangkan HTTP-based database client seperti
@neondatabase/serverlessatau@planetscale/databaseyang pakai connection pooling bawaan. - Di Prisma, atur
connection_limitdi datasource. Jangan pakai default.
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
// Untuk koneksi pooled
relationMode = "prisma"
}
Panduan lebih dalam: Replikasi Database: Kenapa Kamu Butuh Master & Replika.
Jebakan 3: Caching yang Salah Tempat, Salah Waktu
Server components dirender per request secara default. Tanpa caching, tiap request akan memukul database. Ini boros, terutama untuk data yang jarang berubah: daftar kategori, konfigurasi, landing page.
Tapi di sisi lain, caching yang terlalu agresif bisa menyebabkan stale data. User update profil, tapi halaman masih menampilkan data lama selama 10 menit.
Framework caching yang efektif:
- Static data (konfigurasi, enum): cache di level aplikasi atau Redis. TTL panjang, invalidasi saat deployment.
- Session/user data: cache per user, TTL pendek. Jangan share cache antar user.
- Shared data (feed, leaderboard): cache di CDN atau Redis dengan stagger refresh.
- Real-time data: jangan di-cache. Gunakan WebSocket atau streaming.
Di Next.js, manfaatkan unstable_cache dari next/cache untuk data yang benar-benar stabil. Untuk data dinamis, React.cache() mencegah fetch ganda dalam satu request cycle. Ini beda tipis tapi dampaknya besar.
import { cache } from "react";
// Ini mencegah fetch ganda dalam satu render tree
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
Jebakan 4: Authorization yang Cuma Ada di Middleware
Banyak developer menaruh semua logika authorization di middleware Next.js. “Kalau user nggak login, redirect ke login page.” Itu cukup untuk HTTP routing. Tapi nggak cukup untuk data access.
Kenapa? Karena server components dan server actions bisa dipanggil secara terprogram. Middleware cuma jalan di edge untuk routing. Begitu request masuk ke server component, middleware sudah selesai. Kalau authorization hanya ada di middleware, data layer-mu telanjang.
Pola yang benar:
- Middleware: validasi session dasar (ada cookie? signed?), redirect kalau diperlukan.
- Server Component / Data Layer: verifikasi ulang siapa user, apa role-nya, dan apakah dia berhak atas data ini.
- Row-Level Security (RLS): untuk PostgreSQL, manfaatkan RLS sebagai lapisan terakhir yang nggak bisa dilewati.
// JANGAN begini:
export async function getInvoices() {
return db.invoice.findMany(); // semua invoice siapa pun!
}
// TAPI begini:
export async function getInvoices(orgId: string) {
// Verifikasi user punya akses ke org ini
const membership = await verifyOrgAccess(orgId);
if (!membership) throw new Error("Unauthorized");
return db.invoice.findMany({
where: { organizationId: orgId }
});
}
Untuk aplikasi multi-tenant, selalu tambahkan organizationId atau tenantId di setiap query. Jangan pernah mengandalkan filter di frontend. Jangan pernah percaya bahwa “user nggak akan request data tenant lain.”
Jebakan 5: Data Leak Lewat Serialisasi
Server components mengirim data ke client components via props. Data ini diserialisasi. Field yang lupa difilter akan ikut terkirim ke browser. Password hash, token internal, field isAdmin, semuanya bisa bocor.
Ini bukan bug framework. Ini kesalahan desain. Di REST API dulu, kamu sadar bahwa response JSON adalah kontrak publik. Di server components, banyak developer yang lupa bahwa props ke client component juga adalah kontrak publik.
Proteksi:
- Buat Data Transfer Object (DTO) yang eksplisit memilih field mana yang dikirim ke client.
- Gunakan
selectdi Prisma atau.pick()di Drizzle untuk membatasi kolom yang di-fetch. - Jangan pernah mengoper whole row object ke client component.
// Berbahaya: seluruh row dikirim
export async function UserProfile({ userId }: { userId: string }) {
const user = await db.user.findUnique({ where: { id: userId } });
return <ClientProfile user={user} />; // password_hash ikut!
}
// Aman: hanya field publik
export async function UserProfile({ userId }: { userId: string }) {
const user = await db.user.findUnique({
where: { id: userId },
select: { id: true, name: true, avatar: true }
});
return <ClientProfile user={user} />;
}
Artikel terkait: Server Components vs Client Components, Salah Taruh Bisa Bikin App Berat.
Jebakan Bonus: Server Actions Tanpa Rate Limiting
Server actions terlihat seperti fungsi biasa. Tapi sebenarnya mereka adalah endpoint POST publik. Siapa pun bisa memanggilnya berulang kali. Tanpa rate limiting, satu malicious user bisa mengeksekusi ribuan INSERT dalam hitungan detik.
Tambahkan rate limiting di level server action, terutama untuk operasi write. Gunakan @upstash/ratelimit dengan Redis, atau @vercel/kv untuk edge rate limiting.
Rekomendasi bacaan: Edge Functions vs Serverless Functions, Pilih yang Mana Biar Nggak Salah Arsitektur.
FAQ
Apa beda akses database di server component vs API route tradisional?
Server component memungkinkan kamu menulis query langsung di komponen React tanpa membuat endpoint API terpisah. Data di-fetch saat render, bukan saat client fetch. Ini mengurangi boilerplate, tapi menuntut disiplin lebih pada caching dan authorization karena tidak ada lapisan API gateway yang membatasi akses.
Apakah Prisma aman untuk server components Next.js di production?
Aman selama kamu mengonfigurasi connection pooling dengan benar dan tidak memanggil Prisma langsung dari client component. Untuk deployment serverless, pastikan pakai connection pooler eksternal (PgBouncer, Neon, PlanetScale) karena Prisma membuka banyak koneksi internal untuk transaksi dan migrasi.
Bagaimana cara mencegah N+1 query di server components?
Gunakan eager loading (include di Prisma, with di Drizzle) untuk relasi yang selalu dibutuhkan. Untuk data paralel dari sumber berbeda, gunakan Promise.all(). Aktifkan logging query di development untuk mendeteksi N+1 sebelum ke production.
Apakah Row-Level Security di PostgreSQL perlu kalau sudah ada authorization di aplikasi?
Sangat disarankan. Authorization di aplikasi bisa terlewat karena human error atau bug. RLS adalah lapisan pertahanan terakhir di database yang tidak bisa di-bypass. Ini prinsip defense in depth: kalau satu lapisan gagal, lapisan berikutnya masih melindungi data.
Kenapa data di server component Next.js bisa bocor ke client?
Karena props yang dikirim dari server component ke client component diserialisasi menjadi JSON. Field sensitif yang ikut dalam objek props akan otomatis tersedia di browser. Solusinya, selalu seleksi field secara eksplisit dengan select atau buat DTO sebelum data meninggalkan server.
Kesimpulan
Server components mengubah cara kita mengakses database. Bukan cuma soal “fetch di server,” tapi soal siapa yang bertanggung jawab atas keamanan, performa, dan kebocoran data. Semua yang dulu dijaga oleh API gateway kini ada di pundak developer.
Mulai dari lima jebakan ini: N+1 query, connection pooling, caching, authorization berlapis, dan serialisasi data. Kalau kelimanya sudah terkendali, sisanya adalah optimasi yang bikin aplikasi-mu dari “jalan” menjadi “ngebut dan aman.”
Punya pengalaman pahit dengan database di server components? Atau ada pola yang selalu kamu pakai di production? Drop di kolom komentar. Dan kalau kamu mau update artikel arsitektur web modern langsung ke inbox, cek newsletter di bawah.
