Belajar ExpressJS - CRUD dengan PostgreSQL (pg/Prisma)
Episode 10 of 28

Belajar ExpressJS - CRUD dengan PostgreSQL (pg/Prisma)

Membangun CRUD PostgreSQL di Express 5 dengan dua pendekatan: pool pg untuk query langsung, dan Prisma untuk ORM dengan migration, plus perbandingan alur kerja relasional melawan MongoDB.

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

Pendahuluan

Setelah di episode 9 kalian menguasai MongoDB, episode 10 melengkapi sisi relasional: PostgreSQL. Banyak sistem produksi membutuhkan relasi, transaksi, dan integritas yang dijaga database — domain di mana MongoDB kurang kuat dan PostgreSQL unggul.

Kita memakai dua pendekatan agar kalian punya gambaran lengkap: pool pg untuk kontrol penuh atas SQL, dan Prisma untuk ORM dengan migration yang ergonomis. Mengapa keduanya? Karena di lapangan, tim memakai keduanya — driver mentah untuk query kompleks, Prisma untuk kecepatan development. Memahami dua jalur ini membuat kalian tidak tergantung pada satu tool.

Persiapan Database

Pastikan kontainer PostgreSQL lab berjalan (episode 0) dan install kedua package:

Install pg dan Prisma
npm install pg prisma @prisma/client
npx prisma init --datasource-provider postgresql

prisma init membuat prisma/schema.prisma dan mengisi .env dengan placeholder DATABASE_URL.

Pendekatan 1: Pool pg

Membuat Pool

Driver pg memakai connection pool — kumpulan koneksi yang dibuka sekali dan dipakai bergantian. Ini wajib untuk server HTTP yang melayani banyak request paralel:

JSsrc/models/pgPool.js
import pg from "pg"
 
const { Pool } = pg
 
export const pool = new Pool({
  connectionString:
    process.env.DATABASE_URL ||
    "postgres://dev:dev@localhost:5432/expresslab",
  max: 10,
})

max: 10 membatasi jumlah koneksi dalam pool. Tanpa pool, setiap request membuka koneksi baru — lambat dan bisa menghabiskan resource database.

Membuat Tabel

Jalankan migrasi pertama lewat SQL langsung:

Buat tabel users
psql "postgres://dev:dev@localhost:5432/expresslab" \
  -c "CREATE TABLE IF NOT EXISTS users (
       id SERIAL PRIMARY KEY,
       name VARCHAR(100) NOT NULL,
       email TEXT NOT NULL UNIQUE,
       age INT CHECK (age >= 0),
       created_at TIMESTAMPTZ NOT NULL DEFAULT now()
     );"

SERIAL membuat auto-increment id, CHECK menjaga age valid, dan UNIQUE menjaga email unik — integritas dijaga di level database, bukan hanya aplikasi.

CRUD dengan SQL Berparameter

JSsrc/controllers/userController.js - CRUD pg
import { pool } from "../models/pgPool.js"
 
export const createUser = async (req, res) => {
  const { name, email, age } = req.body
  const { rows } = await pool.query(
    `INSERT INTO users (name, email, age)
     VALUES ($1, $2, $3)
     RETURNING *`,
    [name, email, age]
  )
  res.status(201).json({ data: rows[0] })
}
 
export const listUsers = async (req, res) => {
  const { rows } = await pool.query(
    `SELECT id, name, email, age, created_at
     FROM users ORDER BY id`
  )
  res.json({ data: rows })
}
 
export const getUser = async (req, res) => {
  const { rows } = await pool.query(
    "SELECT id, name, email, age FROM users WHERE id = $1",
    [req.params.id]
  )
  if (rows.length === 0) {
    throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
  }
  res.json({ data: rows[0] })
}
 
export const deleteUser = async (req, res) => {
  const { rowCount } = await pool.query(
    "DELETE FROM users WHERE id = $1",
    [req.params.id]
  )
  if (rowCount === 0) {
    throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
  }
  res.status(204).end()
}

Perhatikan $1, $2 — ini parameterized query. Nilai dari req.body tidak pernah disisipkan langsung ke string SQL, sehingga SQL injection tidak mungkin. Inilah alasan terpenting memakai pool pg dengan benar.

Pendekatan 2: Prisma

Schema Prisma

prisma/schema.prisma mendefinisikan model:

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        Int      @id @default(autoincrement())
  name      String   @db.VarChar(100)
  email     String   @unique
  age       Int?
  createdAt DateTime @default(now()) @map("created_at")
  @@map("users")
}

Buat migration dan generate client:

Migrasi Prisma
npx prisma migrate dev --name init

prisma migrate dev membuat file migration SQL, menjalankannya, dan meng-generate Prisma Client — semua dari satu perintah.

CRUD dengan Prisma Client

JSsrc/controllers/userController.js - CRUD Prisma
import { PrismaClient } from "@prisma/client"
 
const prisma = new PrismaClient()
 
export const createUser = async (req, res) => {
  const user = await prisma.user.create({ data: req.body })
  res.status(201).json({ data: user })
}
 
export const listUsers = async (req, res) => {
  const users = await prisma.user.findMany({ orderBy: { id: "asc" } })
  res.json({ data: users })
}
 
export const getUser = async (req, res) => {
  const user = await prisma.user.findUnique({
    where: { id: Number(req.params.id) },
  })
  if (!user) throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
  res.json({ data: user })
}
 
export const updateUser = async (req, res) => {
  const user = await prisma.user.update({
    where: { id: Number(req.params.id) },
    data: req.body,
  })
  res.json({ data: user })
}

Prisma Client adalah type-safe — jika age bertipe Int? (nullable), memasukkan string akan ditolak saat kompilasi bila memakai TypeScript, dan dilempar saat runtime di JavaScript.

Note

Prisma Client sebaiknya di-instantiate sekali dan dibagikan (singleton), bukan dibuat per request — membuat instance baru per request membuang resource. Simpan instance di module dan import di mana saja, seperti contoh di atas.

pg vs Prisma: Kapan Memilih

AspekPool pgPrisma
Kontrol SQLPenuhDibatasi API ORM
Keamanan injectionAman jika parameterizedAman by design
MigrationManualBuilt-in (prisma migrate)
Kecepatan developmentLebih lambat (menulis SQL)Lebih cepat
Query kompleksBebasButuh $queryRaw
Type safetyTidak adaYa

Aturan praktisnya: gunakan Prisma untuk pengembangan cepat dan migrasi yang rapi; turun ke pool pg saat query menjadi terlalu kompleks untuk ORM. Banyak tim produksi memakai keduanya dalam satu codebase.

Common Pitfalls

Lupa .env DATABASE_URL

Prisma membaca DATABASE_URL dari .env. Pastikan isinya benar dan file .env tidak ter-commit (episode 16).

Unique Constraint Menjadi 500

Prisma melempar error P2002 untuk duplikat unique — petakan ke 409 di error handler seperti duplicate key Mongo di episode 9.

Membandingkan Integer dan String

Di pg, id bertipe integer. Number(req.params.id) harus dikonversi sebelum dipakai findUnique — jika tidak, Prisma melempar error tipe. Gabungkan dengan validasi (episode 13) untuk 400 yang bersih.

Tip

Uji endpoint dengan curl memakai urutan yang sama seperti episode 9: create → list → get → update → delete. Bandingkan bentuk error 4xx yang kalian dapat — konsistensi respons yang sama di dua database membuktikan arsitektur error episode 7 bekerja.

Penutup

Episode 10 melengkapi kemampuan database kalian: pool pg untuk SQL parameterized yang aman dari injection, Prisma untuk ORM type-safe dengan migration, dan peta kapan memilih yang mana.

Inti yang harus dibawa pulang:

  • Pool pg membuka koneksi sekali dan memakainya bergantian — wajib untuk server HTTP.
  • Parameterized query ($1) mencegah SQL injection — jangan pernah interpolasi string SQL.
  • Prisma menawarkan migration built-in dan type safety; petakan error unik P2002 ke 409.
  • Perbandingan kontrak data MongoDB vs PostgreSQL menentukan pilihan database.
  • Semua endpoint mengikuti kontrak response yang sama dari episode 7.

Di episode 11 selanjutnya kita akan menyajikan konten: static files & templatingexpress.static, template engine EJS/Pug, dan render halaman server-side. Sampai jumpa di episode 11!

Belajar ExpressJS - CRUD dengan PostgreSQL (pg/Prisma) | Belajar ExpressJS