Belajar Fullstack Developer - Database & ORM
Episode 4 of 28

Belajar Fullstack Developer - Database & ORM

Menghubungkan aplikasi Next.js ke PostgreSQL melalui ORM Prisma: membuat skema, menjalankan migration, melakukan CRUD dari server components dan server actions, serta prinsip query optimization untuk mencegah N+1 dan query lambat.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Setelah di episode 3 kalian membangun fitur fullstack dengan data dummy, pada episode ini kita mengganti dummy tersebut dengan database sungguhan. Kita memakai Prisma — ORM (Object-Relational Mapping) paling populer untuk TypeScript — karena ia memberi kalian keamanan tipe end-to-end: jika kolom berubah di database, error muncul di editor, bukan di production.

Mengapa perlu ORM? Karena menulis SQL mentah di aplikasi adalah sumber bug yang mahal: string concatenation rawan SQL injection, tipe hasil query tidak terdefinisi, dan migration harus dikelola manual. ORM mengotomatiskan semua itu sambil tetap memberi kalian escape hatch ke SQL mentah saat perlu query yang sangat kompleks.

Menyiapkan Prisma

Pastikan PostgreSQL dari episode 0 masih berjalan, lalu install Prisma:

Install Prisma
pnpm add -D prisma
pnpm add @prisma/client
npx prisma init --datasource-provider postgresql

Prisma membuat folder prisma/ dan file .env dengan DATABASE_URL. Sesuaikan isinya dengan container PostgreSQL kalian:

.env
DATABASE_URL="postgresql://postgres:password@localhost:5432/fullstack"

Skema: Bahasa Definisi Data

Skema Prisma adalah tempat kalian mendeskripsikan database dengan tipe aman. Mari buat dua model untuk aplikasi catatan kita:

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  notes     Note[]
  createdAt DateTime @default(now())
}
 
model Note {
  id        String   @id @default(cuid())
  title     String
  body      String
  published Boolean  @default(false)
  authorId  String
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
  @@index([authorId])
}

Perhatikan detail penting:

  • @id @default(cuid()): primary key string unik — tidak perlu menebak UUID.
  • @unique pada email: unique constraint di level database, bukan cuma di aplikasi.
  • Relasi Note.author ↔ User.notes: foreign key authorId otomatis dibuat.
  • @@index([authorId]): index untuk query yang sering memfilter note per user.

Migration: Mengubah Skema Menjadi SQL Nyata

Skema Prisma belum berarti apa-apa sampai di-migrate. Migration menghasilkan file SQL, menjalankannya ke database, dan menyimpan histori perubahan:

Buat dan jalankan migration
npx prisma migrate dev --name init
npx prisma generate

migrate dev menciptakan tabel User dan Note di PostgreSQL serta regenerasi Prisma Client. File SQL hasilnya bisa kalian periksa di prisma/migrations/ — inilah yang dipakai untuk mereproduksi skema yang sama di production (episode 17).

Note

Dua perintah yang sering tertukar: migrate dev dipakai saat mengubah skema di development (membuat + menjalankan migration), sedangkan migrate deploy dipakai untuk menerapkan migration yang sudah ada ke production. Jangan pernah menjalankan migrate dev di production.

CRUD dari Server Components & Actions

Sekarang sambungkan Prisma ke aplikasi Next.js. Buat satu klien Prisma global agar koneksi tidak dibuat ulang setiap request:

src/lib/prisma.ts
import { PrismaClient } from "@prisma/client"
 
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}
 
export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({ log: ["query", "error"] })
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

Lalu CRUD lengkap — baca di server component, tulis di server action:

src/app/notes/page.tsx
import { prisma } from "@/lib/prisma"
 
export default async function NotesPage() {
  const notes = await prisma.note.findMany({
    where: { published: true },
    orderBy: { createdAt: "desc" },
    include: { author: { select: { name: true } } },
  })
  return (
    <ul>
      {notes.map((note) => (
        <li key={note.id}>
          <b>{note.title}</b> — {note.author.name}
        </li>
      ))}
    </ul>
  )
}
src/app/notes/new/page.tsx
import { prisma } from "@/lib/prisma"
import { revalidatePath } from "next/cache"
 
async function createNote(formData: FormData) {
  "use server"
  const title = String(formData.get("title") ?? "")
  const body = String(formData.get("body") ?? "")
  await prisma.note.create({
    data: { title, body, authorId: "user-1" },
  })
  revalidatePath("/notes")
}
 
export default function NewNote() {
  return (
    <form action={createNote}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit">Simpan</button>
    </form>
  )
}

findMany + create adalah dua dari lima operasi CRUD dasar. Sisanya — findUnique, update, delete — memiliki bentuk simetris:

Update dan delete
await prisma.note.update({
  where: { id: noteId },
  data: { title: "Judul baru" },
})
 
await prisma.note.delete({ where: { id: noteId } })

Query Optimization: Menghindari N+1 dan Over-fetch

ORAM membuat menulis query mudah — tapi mudah juga menghasilkan query jelek. Tiga pola yang wajib dikuasai:

1. Select Hanya Kolom yang Dibutuhkan

Prisma findMany default mengambil semua kolom. Untuk daftar ringkas, pilih kolom yang benar-benar dipakai:

Select kolom spesifik
const titles = await prisma.note.findMany({
  where: { published: true },
  select: { id: true, title: true },
})

2. Hindari N+1 dengan Include

Masalah N+1: untuk 100 note, query pertama mengambil 100 note, lalu 100 query tambahan untuk author masing-masing — total 101 query. Solusinya: include (join) dalam satu query.

Include relasi dalam satu query
const notes = await prisma.note.findMany({
  include: { author: { select: { name: true } } },
})

3. Pagination, Bukan Ambil Semua

Jangan pernah mengambil semua baris untuk daftar. Prisma menyediakan skip/take:

Pagination
const page = 1
const perPage = 20
const notes = await prisma.note.findMany({
  skip: (page - 1) * perPage,
  take: perPage,
})

Tip

Aktifkan log query (sudah ada di klien Prisma di atas) dan perhatikan outputnya di terminal saat development. Jika melihat pola query berulang-ulang untuk data yang sama, itu sinyal N+1 atau cache yang hilang — keduanya kita selesaikan di episode 8 dan 15.

Penutup

Inti yang harus dibawa pulang:

  • Prisma memberi type safety end-to-end: perubahan skema terdeteksi di editor.
  • migrate dev untuk development, migrate deploy untuk production — jangan tertukar.
  • CRUD: findMany, findUnique, create, update, delete.
  • Optimasi wajib: select kolom, include untuk menghindari N+1, dan skip/take untuk pagination.
  • Aktifkan log query dan biasakan membaca query yang dihasilkan ORM.

Di episode 5 selanjutnya kita akan membahas API design & integration — kapan harus membuat REST/GraphQL di luar server actions, bagaimana integrasi server-side, dan pola error handling yang konsisten. Sampai jumpa di episode 5!

Belajar Fullstack Developer - Database & ORM | Belajar Fullstack