Stack (kombinasi) yang digunakan pada modul back-end ini adalah:
NodeJS + ExpressJS + TypeScript + Prisma ORM + PostgreSQL
NodeJS + ExpressJS
TypeScript
PostgreSQL
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.
Sebelum memulai proyek express, pastikan kalian telah menginstal NodeJS dan package manager NodeJS seperti npm, yarn, atau pnpm
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
-gperlu digunakan agar yarn dapat digunakan di lokasi manapun pada komputer kalian. -
Periksa versi yarn menggunakan perintah berikut:
yarn -v
Menginstal Package Manager PNPM dapat dilakukan dengan menggunakan perintah dari NPM.
-
Jalankan command berikut pada terminal:
npm install -g pnpm
Catatan: flag
-gperlu digunakan agar pnpm dapat digunakan di lokasi manapun pada komputer kalian. -
Periksa versi pnpm menggunakan perintah berikut:
pnpm -v
Pada bagian ini akkan dijelaskan bagaimana cara melakukan setup project Express + TypeScript. Perhatikan langkah - langkahnya.
- Buka terminal pada IDE kalian, kemudian mulai inisiasi proyek menggunakan
Package Managerpilihan kalian.
# Menggunakan NPM
npm init
# Menggunakan yarn
yarn init
# Menggunakan PNPM
pnpm initJawab pertanyaan yang diberikan. Jawaban dari pertanyaan tidak akan memengaruhi hasil dari proyek. hasilnya akan tampak seperti berikut:
Tambahan: buat file.gitignorekemudian isi dengan teksnode_modulesagar folder node_module tidak ikut masuk ke github.
- Instal dependensi
ExpressJS
# Menggunakan NPM
npm install express
# Menggunakan yarn
yarn add express
# Menggunakan PNPM
pnpm i express- Instal juga dependensi
typescript,@types/node,ts-node,nodemondan@types/expresskali 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 nodemonDependencies 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
- Buat file
tsconfig.jsonuntuk mengatur compiler typescript
npx tsc --initNantinya 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
}
}- Buat folder bernama
srckemudian buat pula file di dalam src dengan namaindex.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
- 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}`);
});- 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
- Jalankan website dengan command berikut
# Menggunakan NPM
npm run start:dev
# Menggunakan yarn
yarn start:dev
# Menggunakan PNPM
pnpm start:dev- Buka website kalian pada
http://localhost:PORTdenganPORTmerupakan nilai PORT yang sudah kalian deklarasikan pada fileindex.ts
-
Selamat, kalian telah membuat endpoint pertama kalian menggunakan
ExpressJS + TypeScript -
Script
builddanstartmerupakan command yang digunakan pada tahap production, namun modul ini tidak akan membahas expressJS pada tahap production.
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.
- Buat folder
routerpadasrckemudian isi dengan fileindex.tsdanfood.router.ts
Strukturnya akan tampak seperti berikut:
./
├───node_modules/
├───src/
│ ├───router/
│ │ ├─food.router.ts
│ │ │ index.ts
│ │
│ │ index.ts
│
│ package.json
│ yarn.lock
- Selanjutnya, kita akan isi
food.router.tsdengan 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;- 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;- Terakhir, gunakan router sebelumnya pada file
src/index.tsdengan 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}`);
});- Sekarang apabila kalian menjalankan websitenya, kalian akan mendapati beberapa endpoint baru.
Selain Get, express juga mendukung berbagai metode lain seperti Post, Patch, Put, dan Delete. Berikut untuk lebih jelasnya.
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}` });
});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 } });
});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 } });
});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 } });
});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` });
});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 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 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 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 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),
},
});
});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.
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, dan204 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, dan304 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, dan404 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, dan503 Service Unavailable.
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. Misalkansuccess get all user dataatauuser 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 (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
| 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 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.
- Masuk ke neon.com
- Buat akun baru, dapat menggunakan akun google, github, microsoft, atau opsi lainnya.

- Atur nama project dan region kalian. Disarankan untuk memilih region Singapore agar tidak terkena latensi yang terlalu besar

- Berikut adalah tampilan dashboard neon setelah selesai melakukan registrasi

- Klik tombol
Connectpada bagian kanan atas, lalu salin dan simpanconnection string-nya
- Perlu diingat bahwa Layanan database gratis neon hanya terbatas pada ukuran
500 MB, jadi jangan sampai database kalian terlalu berat ya...
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.
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
Ekosistem Prisma terdiri dari tiga komponen utama:
- Prisma Schema : schema.prisma Tempat mendefinisikan model data aplikasi. single source of truth untuk skema database.
- Prisma Client : Query builder yang di-generate secara otomatis dan type-safe berdasarkan skema. Inilah yang akan digunakan untuk membaca dan menulis data.
- 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.
# 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 --initJalankan perintah berikut untuk menginisialisasi Prisma:
npx prisma initPerintah ini akan membuat:
- Folder
prisma/dengan fileschema.prisma - File
.envuntuk environment variables
Edit file .env:
DATABASE_URL="postgresql://username:password@localhost:5432/modul_3?schema=public" # Sesuaikan dengan URL dari NEON Database atau PostgreSQL lokalCatatan: Ganti URL sesuai dengan konfigurasi masing-masing.
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
npx prisma migrate dev --name initPerintah ini akan:
- Membuat file migrasi di folder
prisma/migrations - Menjalankan migrasi ke database PostgreSQL
- Generate Prisma Client
npx prisma generatePrisma menyediakan alat visual bernama Prisma Studio yang dapat digunakan untuk melihat dan mengedit data di database web interface. Untuk menjalankannya, gunakan:
npx prisma studioBuat file src/config/prisma.ts:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
export default prisma;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);
});npm run devBuka browser dan akses localhost sesuai dengan PORT masing-masing
- Pastikan sudah memiliki akun Neon dan Login
- Buat project baru pada Neon database

- Klik 'Connect' pada Card Connect to your database

- Salin connection string yang diberikan. Ambil bagian 'postgresql://.....'

- Masukkan pada .env sebagai database URL
DATABASE_URL="postgresql://....."- Jalankan migrasi prisma ke database dengan perintah berikut:
npx prisma migrate dev --name init- Jika berhasil, maka database pada Neon sudah terhubung dengan project Express + Prisma kalian. Cek apakah tabel database sudah terbuat atau belum pada Neon Dashboard

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
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'
});
}
};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'
});
}
};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'
});
}
};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'
});
}
};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'
});
}
};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'
});
}
};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'
});
}
};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'
});
}
};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'
});
}
};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'
});
}
};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;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;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 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 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 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 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" }
}
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 } }
]
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.
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 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' },
],
},
},
})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()
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(),
]);Untuk penjelasan lebih lengkap terkait transaction pada Prisma, silahkan gunakan Dokumentasi Resmi Prisma
Prisma menyediakan beberapa fungsi yang dapat digunakan untuk mengeksekusi query raw.
$queryRawMengembalikan value berupa record database yang diperoleh$executeRawMengembalikan value berupa jumlah row yang terpengaruh$queryRawUnsafeMengembalikan value berupa record database yang diperoleh, namun dalam bentuk string$executeRawUnsafeMengembalikan 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';`;-
Berikut link download Postman untuk melakukan testing API (sangat disarankan dan sangat membantu) https://www.postman.com/downloads/
-
Alternatif lainnya bisa mengggunakan APIDog https://apidog.com/api-doc/
-
Atau apabila tidak ingin mendownload, gunakan Hoppscotch https://hoppscotch.io/









