Dalam aplikasi Next.js modern, salah satu keputusan arsitektur yang paling penting adalah di mana akses database dilakukan. Untuk hampir semua aplikasi produksi, jawabannya adalah: di server, bukan di client. Artinya, query ke PostgreSQL, MySQL, SQL Server, MongoDB, atau database lain tidak boleh dijalankan langsung dari browser pengguna.

Masih banyak developer yang mencoba mengakses database dari komponen client, atau mengira browser dapat langsung menggunakan ORM seperti Prisma. Pendekatan ini adalah anti-pattern karena berisiko dari sisi keamanan, kebocoran kredensial, performa, dan pemisahan tanggung jawab aplikasi.

Artikel ini membahas perbedaan akses database di server dan client pada Next.js 16, alasan teknis di balik praktik yang benar, serta contoh implementasi menggunakan Server Component, Route Handler, dan komponen client yang hanya memanggil endpoint aman.

Mengapa Akses Database Harus Dilakukan di Server?

1. Kredensial database tidak boleh dikirim ke browser

Untuk terhubung ke database, aplikasi biasanya membutuhkan kredensial seperti:

  • host database,
  • port,
  • username,
  • password,
  • connection string.

Jika koneksi database dilakukan dari client, maka informasi ini harus tersedia di browser. Itu berarti siapa pun dapat melihatnya melalui:

  • DevTools,
  • source map,
  • network request,
  • bundle JavaScript yang dikirim ke pengguna.

Begitu kredensial bocor, database Anda berpotensi diakses pihak yang tidak berwenang. Risiko ini jauh lebih besar daripada sekadar bug biasa karena dapat berujung pada pencurian, perubahan, atau penghapusan data.

Prinsip penting: browser adalah lingkungan yang tidak tepercaya. Apa pun yang dikirim ke sana harus dianggap dapat dilihat pengguna.

2. ORM seperti Prisma memang dirancang untuk runtime server

Prisma bukan library yang ditujukan untuk dijalankan di browser. Prisma Client bekerja di lingkungan server seperti Node.js, tempat ia dapat:

  • membaca environment variable secara aman,
  • membuka koneksi ke database,
  • menjalankan query dengan kontrol penuh,
  • mengelola pooling atau lifecycle koneksi sesuai kebutuhan runtime.

Jika Anda mencoba memakainya di Client Component, hasilnya biasanya berupa error build, incompatibility runtime, atau bundle yang tidak valid.

3. Server adalah tempat yang tepat untuk otorisasi dan validasi

Akses data hampir selalu membutuhkan aturan bisnis, misalnya:

  • pengguna hanya boleh melihat datanya sendiri,
  • hanya admin yang boleh menghapus data,
  • input harus divalidasi sebelum disimpan,
  • query harus dibatasi agar tidak membebani sistem.

Semua aturan ini harus diterapkan di server. Jika logika akses data dipindahkan ke client, pengguna dapat memanipulasi request dan mencoba melewati pembatasan tersebut.

Dengan menjalankan akses database di server, Anda dapat menambahkan:

  • autentikasi,
  • otorisasi,
  • validasi input,
  • sanitasi data,
  • rate limiting,
  • audit logging.

4. Performa dan efisiensi lebih baik

Database umumnya berada di jaringan privat, dekat dengan server aplikasi. Jika query dilakukan dari server, latensi biasanya lebih rendah dan arsitektur menjadi lebih efisien.

Selain itu, server bisa:

  • menggabungkan beberapa operasi menjadi satu respons,
  • melakukan caching,
  • membatasi field yang dikirim ke client,
  • menghindari overfetching,
  • menangani error dengan cara yang lebih terkontrol.

Client sebaiknya hanya menerima data yang memang dibutuhkan untuk tampilan antarmuka.

Perbedaan Tanggung Jawab Server dan Client di Next.js 16

Server

Di Next.js 16, sisi server bertanggung jawab untuk:

  • mengakses database,
  • membaca secret dan environment variable sensitif,
  • menjalankan logika bisnis,
  • melakukan autentikasi dan otorisasi,
  • membentuk respons yang aman untuk dikirim ke client.

Client

Sedangkan sisi client bertugas untuk:

  • menampilkan UI,
  • menangani interaksi pengguna,
  • mengelola state lokal,
  • memanggil endpoint server bila perlu,
  • merender data yang sudah aman dikirim dari server.

Dengan pembagian ini, arsitektur aplikasi menjadi lebih mudah dirawat, lebih aman, dan lebih konsisten.

Pola yang Benar: Ambil Data di Server Component

Server Component adalah tempat yang sangat cocok untuk membaca data langsung dari database ketika halaman dirender di server.

Contoh struktur Prisma client:

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma?: PrismaClient
}

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Contoh penggunaan di Server Component:

// app/posts/page.tsx
import { prisma } from '@/lib/prisma'

export default async function PostsPage() {
  const posts = await prisma.post.findMany({
    select: {
      id: true,
      title: true,
      published: true,
    },
    orderBy: {
      id: 'desc',
    },
  })

  return (
    <main>
      <h1>Daftar Post</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            {post.title} - {post.published ? 'Published' : 'Draft'}
          </li>
        ))}
      </ul>
    </main>
  )
}

Pada contoh di atas, query dijalankan di server. Browser hanya menerima HTML dan data hasil render yang memang diperlukan, tanpa mengetahui connection string database.

Pola yang Benar: Gunakan Route Handler untuk Akses dari Client

Jika komponen client perlu mengambil atau mengubah data setelah halaman dimuat, gunakan Route Handler sebagai perantara. Client memanggil endpoint internal, lalu endpoint tersebut yang berkomunikasi dengan database.

Contoh Route Handler:

// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function GET() {
  const posts = await prisma.post.findMany({
    select: {
      id: true,
      title: true,
      published: true,
    },
    orderBy: {
      id: 'desc',
    },
  })

  return NextResponse.json(posts)
}

Lalu dari Client Component:

'use client'

import { useEffect, useState } from 'react'

type Post = {
  id: number
  title: string
  published: boolean
}

export default function PostsClient() {
  const [posts, setPosts] = useState<Post[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    async function loadPosts() {
      try {
        const res = await fetch('/api/posts')
        const data = await res.json()
        setPosts(data)
      } finally {
        setLoading(false)
      }
    }

    loadPosts()
  }, [])

  if (loading) return <p>Memuat...</p>

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Pola ini benar karena browser hanya berbicara ke endpoint aplikasi Anda, bukan ke database secara langsung.

Mengapa Client Tidak Boleh Langsung Query Database?

Berikut beberapa masalah nyata jika Anda memaksakan akses database dari browser:

  • Kredensial bocor: secret tidak bisa disembunyikan di sisi client.
  • Permukaan serangan lebih besar: pengguna dapat mencoba query atau payload yang tidak diinginkan.
  • Bypass aturan bisnis: validasi di client mudah dimanipulasi.
  • Tidak kompatibel dengan ORM server-side: Prisma dan banyak driver database tidak dirancang untuk browser.
  • Masalah jaringan: database produksi sering berada di private network dan memang tidak bisa diakses publik.
  • Sulit diobservasi dan diaudit: kontrol logging dan monitoring lebih baik dilakukan di server.

Kapan Client Boleh Mengambil Data?

Client tetap boleh mengambil data, tetapi bukan langsung ke database. Client boleh mengambil data melalui:

  • Route Handler di Next.js,
  • backend service internal,
  • API gateway,
  • server action atau mekanisme server-side lain yang aman.

Intinya, client hanya berkomunikasi dengan lapisan aplikasi yang Anda kendalikan, bukan dengan database mentah.

Praktik Terbaik yang Disarankan

  1. Simpan semua kredensial di environment variable server
    Jangan pernah mengekspos connection string ke variabel yang dikirim ke browser.
  2. Gunakan Server Component untuk data fetching awal
    Ini cocok untuk halaman yang datanya dibutuhkan saat render pertama.
  3. Gunakan Route Handler untuk interaksi client
    Terutama untuk fetch dinamis, filter, pagination, create, update, dan delete.
  4. Batasi field yang diambil dari database
    Gunakan select agar hanya data yang diperlukan yang dikirim ke client.
  5. Tambahkan validasi dan otorisasi di server
    Jangan mengandalkan validasi UI saja.
  6. Jangan kirim data sensitif tanpa kebutuhan jelas
    Misalnya password hash, token internal, atau metadata administratif.
  7. Kelola error dengan aman
    Jangan mengembalikan detail internal database ke pengguna akhir.

Contoh Alur Arsitektur yang Benar

Alur sederhana yang direkomendasikan pada Next.js 16 adalah sebagai berikut:

  1. Pengguna membuka halaman.
  2. Server Component atau Route Handler menerima request.
  3. Server memverifikasi sesi, role, dan input.
  4. Server menjalankan query ke database.
  5. Server memfilter dan membentuk respons.
  6. Client menampilkan data yang sudah aman.

Dengan alur ini, database tetap berada di belakang lapisan server, sehingga lebih mudah diamankan dan dikontrol.

Kesalahan Umum yang Perlu Dihindari

  • Mengimpor Prisma ke file yang memakai 'use client'.
  • Menyimpan URL database di variabel publik seperti NEXT_PUBLIC_*.
  • Menganggap validasi di form client sudah cukup untuk keamanan.
  • Mengirim seluruh record database padahal UI hanya butuh beberapa field.
  • Tidak membatasi siapa yang boleh memanggil endpoint tulis data.

Penutup

Pada Next.js 16, perbedaan akses database di server dan client bukan sekadar preferensi implementasi, tetapi bagian penting dari desain aplikasi yang aman dan sehat. Database harus diakses dari server, baik melalui Server Component maupun Route Handler. Sementara itu, client hanya bertugas menampilkan UI dan memanggil endpoint yang aman.

Dengan mengikuti pola ini, Anda akan mendapatkan aplikasi yang lebih aman dari kebocoran kredensial, lebih mudah diuji dan dirawat, serta lebih sesuai dengan arsitektur modern Next.js.