Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

README.md

Modul 3 - Express JS dan MongoDB

Pendahuluan

Stack (kombinasi) yang digunakan pada modul back-end ini adalah:

NodeJS + ExpressJS + TypeScript + Prisma ORM + PostgreSQL

  • NodeJS + ExpressJS

node_express

  • TypeScript

ts

  • PostgreSQL

postgre

ExpressJS

ExpressJS merupakan salah satu framework nodeJS populer yang digunakan untuk membuat aplikasi web dan API. Framework ini digunakan dengan tujuan agar developer tidak perlu membuat Back-End from scratch dan menghabiskan waktu mereka hanya untuk mengurus kode - kode inisiasi.

Dengan adanya framework seperti ExpressJS ini, maka developer dapat lebih berfokus pada pengerjaan bussiness logic untuk aplikasi web yang akan dibuat.

Cara Melakukan Inisiasi Project ExpressJS

Sebelum memulai proyek express, pastikan kalian telah menginstal NodeJS dan package manager NodeJS seperti npm, yarn, atau pnpm

Cara Menginstal Yarn (Opsional)

Pada modul sebelumnya, kita telah mengetahui cara menginstal NODE JS. Setelah menginstalnya, kita juga mendapat sebuah package manager yang disebut dengan NPM. Kali ini, kita akan mencoba menginstal package manager yang lain.

Menginstal Package Manager Yarn dapat dilakukan dengan menggunakan perintah dari NPM.

  • Jalankan command berikut pada terminal:

    npm install -g yarn

    Catatan: flag -g perlu digunakan agar yarn dapat digunakan di lokasi manapun pada komputer kalian.

  • Periksa versi yarn menggunakan perintah berikut:

    yarn -v

    yarn_version

Cara Menginstal PNPM (Opsional)

Menginstal Package Manager PNPM dapat dilakukan dengan menggunakan perintah dari NPM.

  • Jalankan command berikut pada terminal:

    npm install -g pnpm

    Catatan: flag -g perlu digunakan agar pnpm dapat digunakan di lokasi manapun pada komputer kalian.

  • Periksa versi pnpm menggunakan perintah berikut:

    pnpm -v

    pnpm_version

Inisiasi Proyek ExpressJS + TypeScript

Pada bagian ini akkan dijelaskan bagaimana cara melakukan setup project Express + TypeScript. Perhatikan langkah - langkahnya.

  1. Buka terminal pada IDE kalian, kemudian mulai inisiasi proyek menggunakan Package Manager pilihan kalian.
# Menggunakan NPM
npm init

# Menggunakan yarn
yarn init

# Menggunakan PNPM
pnpm init

Jawab pertanyaan yang diberikan. Jawaban dari pertanyaan tidak akan memengaruhi hasil dari proyek. hasilnya akan tampak seperti berikut:

yarn_init

Tambahan: buat file .gitignore kemudian isi dengan teks node_modules agar folder node_module tidak ikut masuk ke github.

  1. Instal dependensi ExpressJS
# Menggunakan NPM
npm install express

# Menggunakan yarn
yarn add express

# Menggunakan PNPM
pnpm i express
  1. Instal juga dependensi typescript, @types/node, ts-node, nodemon dan @types/express kali ini dengan flag -D
# Menggunakan NPM
npm install -D typescript @types/node @types/express ts-node nodemon

# Menggunakan yarn
yarn add -D typescript @types/node @types/express ts-node nodemon

# Menggunakan PNPM
pnpm i -D typescript @types/node @types/express ts-node nodemon

Dependencies merupakan list package yang diperlukan oleh aplikasi untuk berjalan (pada tahap production), sementara devDependencies merupakan list package yang digunakan khusus pada saat tahap pengembangan (development) atau testing

  1. Buat file tsconfig.json untuk mengatur compiler typescript
npx tsc --init

Nantinya akan muncul sebuah file bernama tsconfig.json yang isinya berbagai macam konfigurasi yang dapat digunakan pada compiler typescript nantinya.

Namun untuk mempermudah pengaturan, kalian bisa hapus semua isi dari file tsconfig.json kemudian isi dengan pengaturan di bawah ini

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "ES2021",
    "sourceMap": true,
    "noEmit": false,
    "outDir": "./dist",
    "baseUrl": "./src",
    "skipLibCheck": true,
    "strict": true,
    "strictNullChecks": false,
    "noImplicitAny": true,
    "strictBindCallApply": true,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
    "strictPropertyInitialization": false,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true
  }
}
  1. Buat folder bernama src kemudian buat pula file di dalam src dengan nama index.ts

Struktur direktori saat ini seharusnya berbentuk seperti pada di bawah ini.

./
├───node_modules/
├───src/
│     index.ts
│
│ package.json
│ yarn.lock

file yarn.lock akan berbeda - beda sesuai dengan package manager yang kalian gunakan

  1. Lakukan inisiasi express pada file index.ts. file ini akan menjadi file utama kita pada proyek ini
import express from "express";

const app = express();

app.use(express.json());

// check endpoint
app.get("/", (_, response) => {
  response.status(200).send("Server is up and running 💫");
});

const PORT = 4000;
app.listen(PORT, () => {
  console.log(`Express is running on Port ${PORT}`);
});
  1. Siapkan script untuk menjalankan website.

Pada file package.json, tambahkan script sesuai dengan teks di bawah agar website dapat dijalankan.

"scripts": {
  "build": "tsc --build",
  "start": "node ./dist/index.js",
  "start:dev": "nodemon ./src/index.ts"
},

Hasilnya akan tampak seperti ini

pckg_json

  1. Jalankan website dengan command berikut
# Menggunakan NPM
npm run start:dev

# Menggunakan yarn
yarn start:dev

# Menggunakan PNPM
pnpm start:dev
  1. Buka website kalian pada http://localhost:PORT dengan PORT merupakan nilai PORT yang sudah kalian deklarasikan pada file index.ts

init_result

  1. Selamat, kalian telah membuat endpoint pertama kalian menggunakan ExpressJS + TypeScript

  2. Script build dan start merupakan command yang digunakan pada tahap production, namun modul ini tidak akan membahas expressJS pada tahap production.

Express Router

Express Router adalah cara untuk memisahkan dan mengorganisasi rute API dalam aplikasi Express. Dengan menggunakan Router, kita dapat membuat endpoint yang lebih terstruktur dan modular, memungkinkan kita untuk mengelompokkan rute berdasarkan fungsionalitas, misalnya rute untuk "users" atau "products."

Dari proyek yang sudah kita buat sebelumnya, kali ini kita akan mengubah strukturnya menjadi Function Based Structure.

  1. Buat folder router pada src kemudian isi dengan file index.ts dan food.router.ts

Strukturnya akan tampak seperti berikut:

./
├───node_modules/
├───src/
│     ├───router/
│     │   ├─food.router.ts
│     │   │ index.ts
│     │
│     │ index.ts
│
│ package.json
│ yarn.lock
  1. Selanjutnya, kita akan isi food.router.ts dengan beberapa endpoint
import express from "express";

const router = express.Router();

router.get("/pizza", (_, res) => {
  res.status(200).send("Mmm... Pizza... 🍕");
});

router.get("/cookie", (_, res) => {
  res.status(200).send("Get some Cookie... 🍪");
});

router.get("/donut", (_, res) => {
  res.status(200).send("Do Not... 🍩");
});

export default router;
  1. Panggil food router yang sudah dibuat sebelumnya pada file router/index.ts
import express from "express";
const router = express.Router();

import foodRouter from "./food.router";

router.use("/food", foodRouter);

export default router;
  1. Terakhir, gunakan router sebelumnya pada file src/index.ts dengan mengubah sebagian dari isinya
import express from "express";
import router from "./router"; // import routernya

const app = express();

app.use(express.json());

// check endpoint
app.get("/", (_, response) => {
  response.status(200).send("Server is up and running 💫");
});

app.use(router); // tambahkan baris ini untuk menggunakan router

const PORT = 4000;
app.listen(PORT, () => {
  console.log(`Express is running on Port ${PORT}`);
});
  1. Sekarang apabila kalian menjalankan websitenya, kalian akan mendapati beberapa endpoint baru.

new_router

Selain Get, express juga mendukung berbagai metode lain seperti Post, Patch, Put, dan Delete. Berikut untuk lebih jelasnya.

GET

Metode GET digunakan untuk mengambil data dari server. Biasanya, metode ini digunakan untuk membaca atau mengakses data yang tersedia.

Contoh Kode:

// Rute GET untuk mendapatkan semua data pengguna
router.get("/users", (req: Request, res: Response) => {
  res.status(200).json({ message: "Retrieve all users" });
});

// Rute GET untuk mendapatkan data spesifik dari pengguna berdasarkan ID
router.get("/users/:id", (req: Request, res: Response) => {
  const { id } = req.params;
  res.status(200).json({ message: `Retrieve user with ID: ${id}` });
});

POST

Metode POST digunakan untuk mengirim data baru ke server, biasanya digunakan untuk menambah data baru.

Contoh Kode:

// Rute POST untuk menambah pengguna baru
router.post("/users", (req: Request, res: Response) => {
  const { name, email } = req.body;
  res.status(201).json({ message: "User created", data: { name, email } });
});

PATCH

Metode PATCH digunakan untuk memperbarui sebagian data pada sumber daya yang ada. Metode ini digunakan ketika kita hanya ingin memperbarui sebagian dari data.

Contoh Kode:

// Rute PATCH untuk memperbarui sebagian data pengguna berdasarkan ID
router.patch("/users/:id", (req: Request, res: Response) => {
  const { id } = req.params;
  const { email } = req.body;
  res
    .status(200)
    .json({ message: `User with ID: ${id} updated`, data: { email } });
});

PUT

Metode PUT digunakan untuk memperbarui data secara menyeluruh pada sumber daya yang ada. Semua data biasanya ditimpa dengan data baru yang dikirimkan.

Contoh Kode:

// Rute PUT untuk memperbarui data pengguna secara menyeluruh berdasarkan ID
router.put("/users/:id", (req: Request, res: Response) => {
  const { id } = req.params;
  const { name, email } = req.body;
  res
    .status(200)
    .json({ message: `User with ID: ${id} replaced`, data: { name, email } });
});

DELETE

Metode DELETE digunakan untuk menghapus data dari server.

Contoh Kode:

// Rute DELETE untuk menghapus pengguna berdasarkan ID
router.delete("/users/:id", (req: Request, res: Response) => {
  const { id } = req.params;
  res.status(200).json({ message: `User with ID: ${id} deleted` });
});

Express Request

Pada Express, objek Request digunakan untuk menyimpan semua informasi yang dikirimkan oleh klien saat mengakses endpoint tertentu. Objek ini memiliki berbagai properti yang memungkinkan kita untuk mengakses data yang dikirimkan, seperti header, parameter, body, dan query.

Request Head

Request Head atau header permintaan adalah metadata yang dikirimkan dari klien ke server dalam bentuk key-value pair. Header ini dapat digunakan untuk memberikan informasi tambahan seperti jenis konten (Content-Type), otorisasi (Authorization), atau custom header lainnya.

Contoh Kode:

router.get("/headers", (req: Request, res: Response) => {
  const contentType = req.headers["content-type"];
  const authorization = req.headers["authorization"];

  res.status(200).json({
    message: "Headers received",
    headers: { contentType, authorization },
  });
});

Request Body

Request Body berisi data yang dikirimkan oleh klien ke server, biasanya melalui metode POST, PUT, atau PATCH. Body umumnya berisi data yang ingin disimpan atau diperbarui, seperti data pengguna atau data formulir.

Contoh Kode:

router.post("/data", (req: Request, res: Response) => {
  const { name, email } = req.body;

  res.status(201).json({
    message: "Data received",
    data: {
      name,
      email,
    },
  });
});

Request Param

Request Param digunakan untuk mengambil data dari URL yang bersifat dinamis, biasanya pada bagian path. Data ini sering digunakan untuk menentukan sumber daya spesifik, seperti ID pengguna atau produk.

Contoh Kode:

router.get("/users/:id", (req: Request, res: Response) => {
  const { id } = req.params;

  res.status(200).json({
    message: `User with ID: ${id} found`,
  });
});

Request Query

Request Query digunakan untuk mendapatkan parameter tambahan dari URL, biasanya untuk menambahkan filter atau opsi tambahan dalam permintaan. Query parameters ditambahkan setelah tanda ? di URL dan sering digunakan untuk menentukan opsi seperti pencarian atau pagination.

Contoh Kode:

router.get("/search", (req: Request, res: Response) => {
  const { keyword, page, limit } = req.query;

  res.status(200).json({
    message: "Search results",
    filters: {
      keyword,
      page: Number(page),
      limit: Number(limit),
    },
  });
});

Express Response

objek Response digunakan untuk mengirimkan respons dari server ke klien setelah permintaan diproses. Melalui objek ini, kita dapat menentukan data apa yang akan dikembalikan, status kode HTTP, tipe konten, serta mengatur header lain yang dibutuhkan klien. Menggunakan Response, kita juga dapat mengirimkan respons dalam berbagai format, seperti JSON, HTML, atau teks biasa.

Http Status Code

Kode status HTTP adalah angka yang menunjukkan status hasil permintaan klien. Kode ini membantu klien memahami apakah permintaan berhasil, dialihkan, atau mengalami error. Berikut adalah beberapa kategori utama kode status HTTP yang sering digunakan:

  • Code 2XX:

    Kode status 2XX menunjukkan bahwa permintaan berhasil. Kode ini digunakan saat data dikirimkan dengan benar atau saat operasi selesai dengan sukses. Contoh kode 2XX yang sering digunakan adalah 200 OK, 201 Created, dan 204 No Content.

  • Code 3XX:

    Kode status 3XX digunakan untuk mengindikasikan bahwa permintaan harus dialihkan ke lokasi lain. Ini sering digunakan untuk redirect, seperti saat konten berpindah ke URL baru. Contoh kode 3XX yang sering digunakan adalah 301 Moved Permanently, 302 Found, dan 304 Not Modified.

  • Code 4XX:

    Kode status 4XX menunjukkan kesalahan di sisi klien. Kode ini berarti bahwa permintaan tidak dapat diproses karena masalah yang disebabkan oleh klien, seperti kesalahan autentikasi atau data yang tidak valid. Contoh kode 4XX yang sering digunakan adalah 400 Bad Request, 401 Unauthorized, dan 404 Not Found.

  • Code 5XX:

    Kode status 5XX menunjukkan bahwa terjadi kesalahan di sisi server. Hal ini biasanya berarti bahwa server tidak dapat memenuhi permintaan karena masalah internal atau gangguan pada server. Contoh kode 5XX yang sering digunakan adalah 500 Internal Server Error, 502 Bad Gateway, dan 503 Service Unavailable.

Cara Membuat Response yang Sesuai

Response yang baik berarti memiliki informasi yang lengkap dan mudah untuk dibaca. Umumnya response selalu dikembalikan dalam bentuk JSON yang isinya seperti berikut:

  • message: string yang menjelaskan response tersebut. Misalkan success get all user data atau user not found

  • status: Boolean (true / false) yang menunjukkan apakah permintaan berhasil atau tidak. Status seharusnya sudah direpresentasikan dalam bentuk kode HTTP, namun karena variasi kode HTTP sangat banyak, maka status ini berfungsi untuk mengerucutkannya menjadi tanda sukses atau tidak.

  • data: Objek yang berisi semua data yang diminta klien

  • metadata (opsional): Metadata biasanya digunakan pada permintaan yang memiliki pagination dan sejenisnya. Metadata menunjukkan bagaimana kondisi data yang diberikan.

PostgreSQL

PostgreSQL (biasa disebut Posgres) merupakan sistem manajemen basis data relasional (RDBMS) berbasis open-source yang mendukung fitur - fitur lanjutan dibandingkan dengan database SQL pada umumnya.

Posgres memiliki beberapa keunggulan diantaranya,

  • Transaksi ACID Compliance (Atomicity, Consistency, Isolation, Durability). Selengkapnya tentang AICD
  • Mendukung ekstensi JSONB
  • High Performance, Skalabilitas tinggi, dan Relatif lebih aman dibandingkan dengan database SQL lainnya.

PostgreSQL sering digunakan dalam aplikasi besar seperti Instagram, Spotify, Reddit, dan Discord

PostgreSQL VS MySQL

Fitur PostgreSQL MySQL
Lisensi Open Source (PostgreSQL License) Open Source (GPL, dimiliki Oracle)
Kepatuhan SQL Standar Sangat tinggi Cukup baik, tapi beberapa fitur tidak standar
Dukungan JSON JSON & JSONB (sangat kuat) JSON biasa (terbatas)
Transaksi & Concurrency MVCC (Multi-Version Concurrency Control) sangat stabil Cukup baik, tapi tergantung engine (InnoDB)
Kinerja (Write-heavy) Lebih kuat untuk transaksi besar Lebih cepat untuk query sederhana
Ekstensi & Kustomisasi Mendukung ekstensi (misal: PostGIS) Kurang fleksibel
Penggunaan umum Aplikasi kompleks & analitik Aplikasi web ringan-menengah

Neon Database

Neon Database merupakan layanan cloud yang menyediakan layanan database PostgreSQL berbasis cloud. Neon juga menyediakan plan gratis sebesar 500 MB sehingga memungkinkan agar praktikum ini dijalankan tanpa harus mengunduh postgres secara local dahulu.

neon-price

Cara membuat akun Neon

  1. Masuk ke neon.com
  2. Buat akun baru, dapat menggunakan akun google, github, microsoft, atau opsi lainnya. neon-register
  3. Atur nama project dan region kalian. Disarankan untuk memilih region Singapore agar tidak terkena latensi yang terlalu besar neon-region
  4. Berikut adalah tampilan dashboard neon setelah selesai melakukan registrasi neon-dashbaord
  5. Klik tombol Connect pada bagian kanan atas, lalu salin dan simpan connection string-nya neon-connect
  6. Perlu diingat bahwa Layanan database gratis neon hanya terbatas pada ukuran 500 MB, jadi jangan sampai database kalian terlalu berat ya...

Prisma ORM

Pendahuluan

Prisma adalah salah satu library ORM (Object-Relational Mapping) untuk Node.js dan TypeScript yang berfungsi sebagai penghubung antara kode JavaScript dengan database relasional seperti PostgreSQL, MySQL, dan SQLite.

Dengan Prisma, kita tidak perlu menulis query SQL secara manual, cukup menggunakan model dan query berbasis objek.

Mengapa Menggunakan Prisma ORM?

Perhatikan perbandingan kode berikut:

const result = await client.query("SELECT * FROM users WHERE id = $1", [id]);
const user = await prisma.user.findUnique({
  where: { id: id },
});

Tidak hanya Prisma mampu memudahkan kita dalam mengambil record dari database, namun penggunaan ORM juga dapat meningkatkan keamanan kode kita agar terhindar dari beberapa kerentanan seperti SQL Injection

Komponen Utama Prisma

Ekosistem Prisma terdiri dari tiga komponen utama:

  1. Prisma Schema : schema.prisma Tempat mendefinisikan model data aplikasi. single source of truth untuk skema database.
  2. Prisma Client : Query builder yang di-generate secara otomatis dan type-safe berdasarkan skema. Inilah yang akan digunakan untuk membaca dan menulis data.
  3. Prisma Migrate : Alat migrasi database yang secara otomatis membuat file migrasi SQL berdasarkan perubahan pada Prisma Schema, dan menjaga skema database tetap sinkron dengan model data.

Inisiasi Prisma pada Express

1. Instalasi Dependencies

# Dependencies utama
npm install @prisma/client dotenv prisma
# atau
yarn add @prisma/client dotenv prisma
# atau
pnpm i @prisma/client dotenv prisma

# Untuk TypeScript (jika belum init tsconfig)
npx tsc --init

2: Inisialisasi Prisma

Jalankan perintah berikut untuk menginisialisasi Prisma:

npx prisma init

Perintah ini akan membuat:

  • Folder prisma/ dengan file schema.prisma
  • File .env untuk environment variables

3. Konfigurasi Database Connection

Edit file .env:

DATABASE_URL="postgresql://username:password@localhost:5432/modul_3?schema=public" # Sesuaikan dengan URL dari NEON Database atau PostgreSQL lokal

Catatan: Ganti URL sesuai dengan konfigurasi masing-masing.

4. Membuat Schema Database

Edit file prisma/schema.prisma:

// Konfigurasi generator
generator client {
  provider = "prisma-client-js"
}

// Konfigurasi datasource
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Model User
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  password  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  posts     Post[]
}

// Model Post
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Penjelasan Schema:

  • @id: Primary key
  • @default(autoincrement()): Auto increment untuk ID
  • @unique: Nilai harus unik/tidak boleh ada yang sama
  • @default(now()): Default value timestamp saat ini
  • @updatedAt: Otomatis update saat record diubah
  • @relation: Mendefinisikan relasi antar tabel

5. Membuat Migrasi Database

npx prisma migrate dev --name init

Perintah ini akan:

  1. Membuat file migrasi di folder prisma/migrations
  2. Menjalankan migrasi ke database PostgreSQL
  3. Generate Prisma Client

6. Generate Prisma Client

npx prisma generate

Prisma menyediakan alat visual bernama Prisma Studio yang dapat digunakan untuk melihat dan mengedit data di database web interface. Untuk menjalankannya, gunakan:

npx prisma studio

7. Setup Prisma Client di Project

Buat file src/config/prisma.ts:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error'],
});

export default prisma;

8. Membuat Express Server

Buat file src/index.ts:

import express, { Application, Request, Response } from 'express';
import dotenv from 'dotenv';
import prisma from './config/prisma';

// Load environment variables
dotenv.config();

const app: Application = express();
const PORT = process.env.PORT || 3000; // Sesuaikan PORT masing-masing

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Test route
app.get('/', (req: Request, res: Response) => {
  res.json({ message: 'Express + Prisma + PostgreSQL' });
});

// Start server
app.listen(PORT, () => {
  console.log(`Server berjalan di http://localhost:${PORT}`);
});

// Graceful shutdown
process.on('SIGINT', async () => {
  await prisma.$disconnect();
  console.log('Database disconnected');
  process.exit(0);
});

9. Menjalankan Aplikasi

npm run dev

Buka browser dan akses localhost sesuai dengan PORT masing-masing


Cara menghubungkan Prisma + Express dengan Neon Database

  1. Pastikan sudah memiliki akun Neon dan Login
  2. Buat project baru pada Neon database Buat project baru
  3. Klik 'Connect' pada Card Connect to your database Connect to your database
  4. Salin connection string yang diberikan. Ambil bagian 'postgresql://.....' Connection String
  5. Masukkan pada .env sebagai database URL
DATABASE_URL="postgresql://....."
  1. Jalankan migrasi prisma ke database dengan perintah berikut:
npx prisma migrate dev --name init
  1. Jika berhasil, maka database pada Neon sudah terhubung dengan project Express + Prisma kalian. Cek apakah tabel database sudah terbuat atau belum pada Neon Dashboard Check tabel pada Neon

CRUD Menggunakan Prisma

Struktur Folder Project

modul-4/
├── prisma/
│   ├── migrations/
│   └── schema.prisma
├── src/
│   ├── config/
│   │   └── prisma.ts
│   ├── controllers/
│   │   ├── userController.ts
│   │   └── postController.ts
│   ├── routes/
│   │   ├── userRoutes.ts
│   │   └── postRoutes.ts
│   └── index.ts
├── .env
├── package.json
└── tsconfig.json

1. CREATE - Membuat Data

A. User Controller

Buat file src/controllers/userController.ts:

import { Request, Response } from 'express';
import prisma from '../config/prisma';

export const createUser = async (req: Request, res: Response) => {
  try {
    const { email, name, password } = req.body;

    // Validasi input
    if (!email || !password) {
      return res.status(400).json({ 
        error: 'Email dan password wajib diisi' 
      });
    }

    // Cek apakah email sudah ada
    const existingUser = await prisma.user.findUnique({
      where: { email }
    });

    if (existingUser) {
      return res.status(400).json({ 
        error: 'Email sudah terdaftar' 
      });
    }

    // Buat user baru
    const user = await prisma.user.create({
      data: {
        email,
        name,
        password // NOTE: hash password terlebih dahulu untuk real-case
      },
      select: {
        id: true,
        email: true,
        name: true,
        createdAt: true
      }
    });

    res.status(201).json({
      message: 'User berhasil dibuat',
      data: user
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat membuat user' 
    });
  }
};

B. Post Controller

Buat file src/controllers/postController.ts:

import { Request, Response } from 'express';
import prisma from '../config/prisma';

export const createPost = async (req: Request, res: Response) => {
  try {
    const { title, content, authorId } = req.body;

    // Validasi input
    if (!title || !authorId) {
      return res.status(400).json({ 
        error: 'Title dan authorId wajib diisi' 
      });
    }

    // Cek apakah user ada
    const user = await prisma.user.findUnique({
      where: { id: Number(authorId) }
    });

    if (!user) {
      return res.status(404).json({ 
        error: 'User tidak ditemukan' 
      });
    }

    // Buat post baru
    const post = await prisma.post.create({
      data: {
        title,
        content,
        authorId: Number(authorId)
      },
      include: {
        author: {
          select: {
            id: true,
            name: true,
            email: true
          }
        }
      }
    });

    res.status(201).json({
      message: 'Post berhasil dibuat',
      data: post
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat membuat post' 
    });
  }
};

2. READ - Membaca Data

A. Get All Users

Tambahkan di src/controllers/userController.ts:

export const getAllUsers = async (req: Request, res: Response) => {
  try {
    const users = await prisma.user.findMany({
      select: {
        id: true,
        email: true,
        name: true,
        createdAt: true,
        _count: {
          select: { posts: true }
        }
      }
    });

    res.json({
      message: 'Berhasil mengambil data users',
      data: users
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat mengambil data users' 
    });
  }
};

B. Get User By ID

Tambahkan di src/controllers/userController.ts:

export const getUserById = async (req: Request, res: Response) => {
  try {
    const { id } = req.params;

    const user = await prisma.user.findUnique({
      where: { id: Number(id) },
      select: {
        id: true,
        email: true,
        name: true,
        createdAt: true,
        posts: {
          select: {
            id: true,
            title: true,
            published: true,
            createdAt: true
          }
        }
      }
    });

    if (!user) {
      return res.status(404).json({ 
        error: 'User tidak ditemukan' 
      });
    }

    res.json({
      message: 'Berhasil mengambil data user',
      data: user
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat mengambil data user' 
    });
  }
};

C. Get All Posts

Tambahkan di src/controllers/postController.ts:

export const getAllPosts = async (req: Request, res: Response) => {
  try {
    const posts = await prisma.post.findMany({
      include: {
        author: {
          select: {
            id: true,
            name: true,
            email: true
          }
        }
      },
      orderBy: {
        createdAt: 'desc'
      }
    });

    res.json({
      message: 'Berhasil mengambil data posts',
      data: posts
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat mengambil data posts' 
    });
  }
};

D. Get Post By ID

Tambahkan di src/controllers/postController.ts:

export const getPostById = async (req: Request, res: Response) => {
  try {
    const { id } = req.params;

    const post = await prisma.post.findUnique({
      where: { id: Number(id) },
      include: {
        author: {
          select: {
            id: true,
            name: true,
            email: true
          }
        }
      }
    });

    if (!post) {
      return res.status(404).json({ 
        error: 'Post tidak ditemukan' 
      });
    }

    res.json({
      message: 'Berhasil mengambil data post',
      data: post
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat mengambil data post' 
    });
  }
};

3. UPDATE - Mengubah Data

A. Update User

Tambahkan di src/controllers/userController.ts:

export const updateUser = async (req: Request, res: Response) => {
  try {
    const { id } = req.params;
    const { email, name } = req.body;

    // Cek apakah user ada
    const existingUser = await prisma.user.findUnique({
      where: { id: Number(id) }
    });

    if (!existingUser) {
      return res.status(404).json({ 
        error: 'User tidak ditemukan' 
      });
    }

    // Update user
    const user = await prisma.user.update({
      where: { id: Number(id) },
      data: {
        ...(email && { email }),
        ...(name && { name })
      },
      select: {
        id: true,
        email: true,
        name: true,
        updatedAt: true
      }
    });

    res.json({
      message: 'User berhasil diupdate',
      data: user
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat update user' 
    });
  }
};

B. Update Post

Tambahkan di src/controllers/postController.ts:

export const updatePost = async (req: Request, res: Response) => {
  try {
    const { id } = req.params;
    const { title, content, published } = req.body;

    // Cek apakah post ada
    const existingPost = await prisma.post.findUnique({
      where: { id: Number(id) }
    });

    if (!existingPost) {
      return res.status(404).json({ 
        error: 'Post tidak ditemukan' 
      });
    }

    // Update post
    const post = await prisma.post.update({
      where: { id: Number(id) },
      data: {
        ...(title && { title }),
        ...(content !== undefined && { content }),
        ...(published !== undefined && { published })
      },
      include: {
        author: {
          select: {
            id: true,
            name: true,
            email: true
          }
        }
      }
    });

    res.json({
      message: 'Post berhasil diupdate',
      data: post
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat update post' 
    });
  }
};

4. DELETE - Menghapus Data

A. Delete User

Tambahkan di src/controllers/userController.ts:

export const deleteUser = async (req: Request, res: Response) => {
  try {
    const { id } = req.params;

    // Cek apakah user ada
    const existingUser = await prisma.user.findUnique({
      where: { id: Number(id) },
      include: {
        _count: {
          select: { posts: true }
        }
      }
    });

    if (!existingUser) {
      return res.status(404).json({ 
        error: 'User tidak ditemukan' 
      });
    }

    // Hapus semua post user terlebih dahulu (cascade delete)
    await prisma.post.deleteMany({
      where: { authorId: Number(id) }
    });

    // Hapus user
    await prisma.user.delete({
      where: { id: Number(id) }
    });

    res.json({
      message: 'User berhasil dihapus'
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat menghapus user' 
    });
  }
};

B. Delete Post

Tambahkan di src/controllers/postController.ts:

export const deletePost = async (req: Request, res: Response) => {
  try {
    const { id } = req.params;

    // Cek apakah post ada
    const existingPost = await prisma.post.findUnique({
      where: { id: Number(id) }
    });

    if (!existingPost) {
      return res.status(404).json({ 
        error: 'Post tidak ditemukan' 
      });
    }

    // Hapus post
    await prisma.post.delete({
      where: { id: Number(id) }
    });

    res.json({
      message: 'Post berhasil dihapus'
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ 
      error: 'Terjadi kesalahan saat menghapus post' 
    });
  }
};

5. Setup Routes

A. User Routes

Buat file src/routes/userRoutes.ts:

import { Router } from 'express';
import {
  createUser,
  getAllUsers,
  getUserById,
  updateUser,
  deleteUser
} from '../controllers/userController';

const router = Router();

router.post('/', createUser);
router.get('/', getAllUsers);
router.get('/:id', getUserById);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);

export default router;

B. Post Routes

Buat file src/routes/postRoutes.ts:

import { Router } from 'express';
import {
  createPost,
  getAllPosts,
  getPostById,
  updatePost,
  deletePost
} from '../controllers/postController';

const router = Router();

router.post('/', createPost);
router.get('/', getAllPosts);
router.get('/:id', getPostById);
router.put('/:id', updatePost);
router.delete('/:id', deletePost);

export default router;

6. Update Main Server

Edit file src/index.ts:

import express, { Application, Request, Response } from 'express';
import dotenv from 'dotenv';
import prisma from './config/prisma';
import userRoutes from './routes/userRoutes';
import postRoutes from './routes/postRoutes';

// Load environment variables
dotenv.config();

const app: Application = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.get('/', (req: Request, res: Response) => {
  res.json({ 
    message: 'Express + Prisma + PostgreSQL API',
    endpoints: {
      users: '/api/users',
      posts: '/api/posts'
    }
  });
});

app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);

// 404 handler
app.use((req: Request, res: Response) => {
  res.status(404).json({ error: 'Endpoint tidak ditemukan' });
});

// Start server
app.listen(PORT, () => {
  console.log(`Server berjalan di http://localhost:${PORT}`);
});

// Graceful shutdown
process.on('SIGINT', async () => {
  await prisma.$disconnect();
  console.log('Database disconnected');
  process.exit(0);
});

Filtering & Pagination

Filtering

Filtering digunakan untuk menyaring data berdasarkan kondisi tertentu sebelum dikembalikan dari database. Prisma menyediakan parameter where yang sangat fleksibel, mirip seperti klausa WHERE pada SQL.

Contoh saat ingin mengambil semua post yang sudah dipublish:

const publishedPosts = await prisma.post.findMany({
  where: {
    published: true,
  },
});

Atau mengambil user dengan nama yang mengandung kata “budi”:

const filteredUsers = await prisma.user.findMany({
  where: {
    name: {
      contains: 'budi',
      mode: 'insensitive', // agar tidak case-sensitive
    },
  },
});

mode: 'insensitive' membuat pencarian tidak membedakan huruf besar dan kecil (case-insensitive). Misalnya pencarian “budi” akan cocok dengan “Budi”, “BUdi”, atau “bUDI”.

Contoh Filtering dengan Beberapa Kondisi (AND, OR, NOT):

const complexFilter = await prisma.post.findMany({
  where: {
    AND: [
      { published: true },
      { title: { contains: 'tutorial' } },
    ],
  },
});
const posts = await prisma.post.findMany({
  where: {
    OR: [
      { title: { contains: 'AI' } },
      { title: { contains: 'Machine Learning' } },
    ],
    NOT: { authorId: 1 },
  },
});

Bisa juga menerapkan filtering berdasarkan relasi, contohnya menampilkan semua post dari user dengan email tertentu:

const postsByEmail = await prisma.post.findMany({
  where: {
    author: {
      email: 'budi@santoso.id',
    },
  },
});

Pagination

Pagination digunakan untuk mengatur jumlah data yang ditampilkan per halaman. Prisma menyediakan dua properti utama:

  • skip: untuk melewati sejumlah data
  • take: untuk menentukan berapa banyak data yang diambil

Contoh jika ingin mengambil 10 post per halaman:

const page = Number(req.query.page) || 1; // halaman saat ini
const limit = Number(req.query.limit) || 10; // jumlah per halaman

const posts = await prisma.post.findMany({
  skip: (page - 1) * limit,
  take: limit,
  orderBy: {
    createdAt: 'desc',
  },
});

Lalu mengembalikan hasil dan info pagination ke client:

const totalPosts = await prisma.post.count();

res.json({
  message: 'Berhasil mengambil data posts dengan pagination',
  currentPage: page,
  totalPages: Math.ceil(totalPosts / limit),
  totalPosts,
  data: posts,
});

Filtering + Pagination

Filtering dan pagination bisa dikombinasikan agar lebih dinamis. Misalnya client ingin mencari post yang mengandung kata “eco” pada judulnya, dengan pagination:

const { page = 1, limit = 5, search = '' } = req.query;

const posts = await prisma.post.findMany({
  where: {
    title: {
      contains: search as string,
      mode: 'insensitive',
    },
  },
  skip: (Number(page) - 1) * Number(limit),
  take: Number(limit),
  orderBy: {
    createdAt: 'desc',
  },
  include: {
    author: {
      select: {
        id: true,
        name: true,
        email: true,
      },
    },
  },
});

const total = await prisma.post.count({
  where: {
    title: {
      contains: search as string,
      mode: 'insensitive',
    },
  },
});

res.json({
  message: 'Berhasil mengambil data posts dengan filter & pagination',
  currentPage: Number(page),
  totalPages: Math.ceil(total / Number(limit)),
  totalPosts: total,
  data: posts,
});

Contoh ini sangat cocok diletakkan di controller getAllPosts, menggantikan query statis agar lebih fleksibel untuk pencarian dan pagination dari frontend.

Aggregate function

Aggregate Dasar

Aggregate digunakan untuk melakukan perhitungan matematis terhadap kolom tertentu di tabel database, mirip seperti COUNT, AVG, SUM, MIN, dan MAX pada SQL. Prisma menyediakan fungsi bawaan seperti:

Fungsi Deskripsi
_count Menghitung jumlah record
_avg Menghitung nilai rata-rata
_sum Menjumlahkan nilai
_min Nilai minimum
_max Nilai maksimum

Contoh menghitung total user

const totalUsers = await prisma.user.count();
console.log(`Total User: ${totalUsers}`);

Contoh aggregate lengkap

const result = await prisma.post.aggregate({
  _count: { id: true },
  _avg: { authorId: true },
  _min: { createdAt: true },
  _max: { createdAt: true },
});
console.log(result);

Menghasilkan object berisi jumlah post, rata-rata authorId, waktu paling awal, dan paling baru. Contoh :

{
  "_count": { "id": 10 },
  "_avg": { "authorId": 2.5 },
  "_min": { "createdAt": "2025-10-10T00:00:00Z" },
  "_max": { "createdAt": "2025-10-15T00:00:00Z" }
}
Group By

groupBy digunakan untuk mengelompokkan data berdasarkan kolom tertentu dan melakukan agregasi di tiap grup. Contoh menghitung jumlah post per author

const groupedPosts = await prisma.post.groupBy({
  by: ['authorId'],
  _count: { id: true },
  orderBy: {
    _count: { id: 'desc' },
  },
});

console.log(groupedPosts);

Menghasilkan daftar jumlah postingan dari tiap penulis (authorId). Contoh Output:

[
  { "authorId": 1, "_count": { "id": 5 } },
  { "authorId": 2, "_count": { "id": 3 } }
]

Aggregate dengan Kondisi (Filter)

Anda juga bisa menggabungkan aggregate dengan where untuk perhitungan bersyarat.

Contoh menghitung jumlah post yang telah dipublish

const totalPublished = await prisma.post.count({
  where: {
    published: true,
  },
});
console.log(`Jumlah post yang sudah dipublish: ${totalPublished}`);

Contoh mengambil statistik berdasarkan tanggal

const dailyStats = await prisma.post.groupBy({
  by: ['createdAt'],
  _count: { id: true },
  orderBy: {
    createdAt: 'asc',
  },
});

Menampilkan jumlah post per tanggal pembuatan.

Batch Queries & Transaction

Transaction merupakan sebuah rangkaian query yang dieksekusi sebagai satu kesatuan logis. Hal ini berarti semua perintah dalam satu transaction harus berhasil seluruhnya, jika tidak maka tidak ada perintah yang akan dieksekusi.

Pada Prisma, beberapa fungsi berikut menggunakan transaction dalam prosesnya,

  • Nested writes
  • Batch / Bulk Writes
  • $transaction

Nested Writes

Nested write memungkinkan untuk melakukan beberapa operasi yang memiliki target beberapa record pada database. Misalkan membuat beberapa postingan di saat membuat record user baru. Prisma akan memastikan semua operasi tersebut sukses atau gagal seluruhnya (Record tidak akan memungkinkan untuk setengah terbuat).

Contoh (Membuat beberapa post saat membuat user):

const newUser: User = await prisma.user.create({
  data: {
    email: 'budi@santoso.id',
    posts: {
      create: [
        { title: 'Judul Post 1' },
        { title: 'Judul Post 2' },
      ],
    },
  },
})

Batch / Bulk Writes

Bulk Write memungkinkan untuk menulis record dengan tipe yang sama dalam satu transaction. Untuk lebih jelasnya, perhatikan beberapa fungsi berikut:

  • createMany()
  • createManyAndReturn()
  • updateMany()
  • updateManyAndReturn()
  • deleteMany()

Misalkan kita ingin memperbarui semua harga buku dari author dengan id 5, maka kita dapat menuliskannya secara langsung dengan memanfaatkan fungsi updateMany()

await prisma.book.updateMany({
  where: {
    author: {
      id: 5,
    }
  },
  data: {
    price: {
      increment: 1000
    },
  },
});

Fungsi di atas akan mengembalikan value dengan tipe number, karena fungsi updateMany() tidak mengembalikan data, melainkan jumlah record yang ter-update.

Apabila ingin mengembalikan data pula, gunakan fungsi updateManyAndReturn()

$transaction

fungsi $transaction memungkinkan untuk menjalankan beberapa fungsi prisma sekaligus dalam satu query, yang berarti mengurangi waktu eksekusi.

Untuk lebih jelasnya, perhatikan fungsi berikut. Fungsi ini bertujuan untuk dua hal, yaitu mengembalikan semua post yang memiliki kata kucing pada judulnya dan menghitung jumlah post yang ada pada database.

const [posts, totalPosts] = await prisma.$transaction([
  prisma.post.findMany({ where: { title: { contains: 'kucing' } } }),
  prisma.post.count(),
]);

Penjelasan Lebih Lengkap

Untuk penjelasan lebih lengkap terkait transaction pada Prisma, silahkan gunakan Dokumentasi Resmi Prisma

Raw Query menggunakan Prisma

Prisma menyediakan beberapa fungsi yang dapat digunakan untuk mengeksekusi query raw.

  • $queryRaw Mengembalikan value berupa record database yang diperoleh
  • $executeRaw Mengembalikan value berupa jumlah row yang terpengaruh
  • $queryRawUnsafe Mengembalikan value berupa record database yang diperoleh, namun dalam bentuk string
  • $executeRawUnsafe Mengembalikan value berupa jumlah row yang terpengaruh, namun dalam bentuk string

Untuk menjalankan query raw, cukup masukkan query sql yang akan dieksekusi ke dalam fungsi dengan bentuk string

import { User } from "@prisma/client";

const result = await prisma.$queryRaw<User[]>`SELECT * FROM User`;

// result akan memiliki tipedata User[]

Perlu diperhatikan bahwa variabel tidak dapat digunakan dalam string literal SQL.

Fungsi berikut tidak akan mencari nilai string Ini Budi bukan Buni melainkan Ini bukan Buni

const name = "Budi";
await prisma.$queryRaw`SELECT 'Ini ${name} bukan Buni';`;

Sebagai alternatifnya, jadikan seluruh string sebagai variabel

const name = "Ini Budi bukan Buni";
await prisma.$queryRaw`SELECT ${name};`;

atau gunakan fungsi concat string pada SQL

const name = "Budi";
await prisma.$queryRaw`SELECT 'Ini ' || ${name} || ' bukan Buni';`;

Referensi

Aplikasi Testing API