Appearance
Chapter 3 - PostgreSQL dan TypeORM
Ikhtisar Chapter
Chapter ini mengubah aplikasi dari penyimpanan sementara di memory menjadi aplikasi yang menyimpan data secara permanen di PostgreSQL. TypeORM dipakai sebagai jembatan antara kode TypeScript dan database relasional.
Peta Cepat
FokusPostgreSQL, Docker, TypeORM, entity, repository
Perubahan BesarService tidak lagi membaca array, tetapi memanggil repository async
Hasil AkhirCRUD task berjalan dengan database dan query builder
Gambaran Besar
Pada chapter sebelumnya, data disimpan dalam array. Cara itu cocok untuk belajar alur NestJS, tetapi tidak cukup untuk aplikasi nyata. Begitu server restart, semua data hilang. Persistensi berarti data tetap ada karena disimpan di database.
Kursus menggunakan PostgreSQL sebagai database relasional dan TypeORM sebagai ORM. Dengan ORM, kita mendefinisikan entity sebagai class TypeScript, lalu menggunakan repository untuk melakukan operasi database tanpa menulis SQL mentah di setiap tempat.
Alur Persistensi
text
Controller
|
v
TasksService
|
v
TasksRepository
|
v
TypeORM Query atau Repository Method
|
v
PostgreSQLStruktur Direktori
Chapter ini menambahkan beberapa file baru dan mengubah penyimpanan data dari array ke database:
src/
├── main.ts
├── app.module.ts ← DIUBAH: + TypeOrmModule.forRoot()
└── tasks/
├── dto/
│ ├── create-task.dto.ts
│ ├── get-tasks-filter.dto.ts
│ └── update-task-status.dto.ts
├── task.entity.ts ← BARU: class Task dengan @Entity, @Column
│ menggantikan task.model.ts (interface)
├── task-status.enum.ts ← tetap ada, dipakai entity dan repository
├── tasks.controller.ts
├── tasks.module.ts ← DIUBAH: + TypeOrmModule.forFeature([Task])
├── tasks.repository.ts ← BARU: extends Repository<Task>
└── tasks.service.ts ← DIUBAH: semua method jadi async/awaitPackage Baru
bash
yarn add @nestjs/typeorm typeorm pgTiga package ini diperlukan: @nestjs/typeorm untuk integrasi NestJS, typeorm untuk ORM-nya sendiri, pg untuk driver PostgreSQL.
Ringkasan Lecture
1. Introduction to Persistence
Persistensi artinya data tetap ada setelah server restart. Sebelum chapter ini, semua task disimpan di array dalam memori — begitu server mati, semua data hilang. Ini tidak cocok untuk aplikasi nyata.
PostgreSQL dipilih sebagai database relasional. Alasan umum memilih relasional: data terstruktur dengan relasi antar tabel yang terdefinisi, transaksi ACID tersedia, dan dukungan komunitas sangat besar.
TypeORM berfungsi sebagai jembatan antara TypeScript dan PostgreSQL. Daripada menulis SQL mentah di setiap method, kita mendefinisikan entity sebagai class TypeScript, dan TypeORM menerjemahkannya ke operasi database.
2. Running PostgreSQL via Docker
Daripada menginstal PostgreSQL langsung, Docker dipakai agar setup lebih bersih dan dapat diulang di mesin mana pun:
bash
docker run \
--name postgres-nest \
-p 5432:5432 \
-e POSTGRES_PASSWORD=postgres \
-d postgresPenjelasan flag:
--name postgres-nest— nama container untuk memudahkan manajemen-p 5432:5432— port host ke port container (formathost:container)-e POSTGRES_PASSWORD=postgres— set password user postgres-d— jalankan sebagai daemon (background)
Verifikasi container berjalan:
bash
docker ps
# CONTAINER ID IMAGE ... PORTS NAMES
# abc123 postgres ... 0.0.0.0:5432->5432/tcp postgres-nest3. Setting up pgAdmin
pgAdmin adalah GUI untuk mengelola database PostgreSQL. Setelah pgAdmin dibuka di browser, tambahkan koneksi baru dengan konfigurasi:
Host: localhost
Port: 5432
Username: postgres
Password: postgrespgAdmin membantu melihat tabel yang dibuat, menjalankan query SQL manual, dan memeriksa data saat debugging.
4. Creating a Database using pgAdmin
Buat database baru bernama task-management melalui pgAdmin. Nama ini akan dipakai di konfigurasi TypeORM. Satu PostgreSQL instance dapat menampung banyak database — tiap proyek biasanya punya database sendiri.
5. Introduction to TypeORM
TypeORM adalah ORM (Object Relational Mapper) untuk Node.js dan TypeScript. Dengan ORM:
- Tabel database direpresentasikan sebagai class TypeScript (entity)
- Kolom direpresentasikan sebagai property dengan decorator
- Query dibuat melalui method atau query builder, bukan string SQL mentah
- TypeScript mendeteksi kesalahan property lebih awal saat compile time
Kekurangan yang perlu diingat: meski ORM menyederhanakan banyak hal, pemahaman SQL tetap perlu agar query yang dihasilkan efisien dan tidak N+1.
6. Setting up a Database Connection
Install tiga package yang dibutuhkan:
bash
yarn add @nestjs/typeorm typeorm pgDaftarkan TypeOrmModule di AppModule:
typescript
// src/app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'task-management',
autoLoadEntities: true, // entity yang didaftarkan lewat forFeature() otomatis dimuat
synchronize: true, // schema disesuaikan otomatis — JANGAN di production!
}),
TasksModule,
],
})
export class AppModule {}synchronize: true
synchronize: true secara otomatis mengubah schema database sesuai entity. Di development ini sangat nyaman, tetapi di production bisa menghapus kolom atau data yang tidak terduga. Gunakan migrasi di production.
7. Creating a Task Entity
Task diubah dari interface menjadi entity TypeORM. Decorator dari typeorm mendefinisikan tabel dan kolomnya:
typescript
// src/tasks/task.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
import { TaskStatus } from './task-status.enum'
@Entity() // nama tabel default: 'task' (lowercase class name)
export class Task {
@PrimaryGeneratedColumn('uuid') // UUID dibuat otomatis oleh database
id: string
@Column()
title: string
@Column()
description: string
@Column()
status: TaskStatus // disimpan sebagai string enum
}Pindahkan TaskStatus ke file terpisah agar bisa di-import baik oleh entity maupun service:
typescript
// src/tasks/task-status.enum.ts
export enum TaskStatus {
OPEN = 'OPEN',
IN_PROGRESS = 'IN_PROGRESS',
DONE = 'DONE',
}8. Active Record vs Data Mapper
Dua pola populer untuk ORM:
Active Record — entity mewarisi class base repository, method database langsung ada di entity:
typescript
// Active Record style
const task = new Task()
task.title = 'Beli susu'
await task.save() // entity punya method save()
const tasks = await Task.find() // query via static methodData Mapper — entity hanya data, repository terpisah menangani akses database:
typescript
// Data Mapper style
const task = this.tasksRepository.create({ title: 'Beli susu' })
await this.tasksRepository.save(task)
const tasks = await this.tasksRepository.find()NestJS merekomendasikan Data Mapper karena:
- Entity tetap bersih dari logika database
- Lebih mudah di-mock saat testing
- Sesuai dengan prinsip separation of concerns
9. Creating a Tasks Repository
Buat custom repository untuk mengelompokkan semua query terkait task:
typescript
// src/tasks/tasks.repository.ts
import { DataSource, Repository } from 'typeorm'
import { Injectable } from '@nestjs/common'
import { Task } from './task.entity'
import { CreateTaskDto } from './dto/create-task.dto'
import { TaskStatus } from './task-status.enum'
@Injectable()
export class TasksRepository extends Repository<Task> {
constructor(private dataSource: DataSource) {
super(Task, dataSource.createEntityManager())
}
async createTask(createTaskDto: CreateTaskDto): Promise<Task> {
const { title, description } = createTaskDto
const task = this.create({
title,
description,
status: TaskStatus.OPEN,
})
await this.save(task)
return task
}
}Daftarkan repository dan entity di TasksModule:
typescript
// src/tasks/tasks.module.ts
import { TypeOrmModule } from '@nestjs/typeorm'
import { Task } from './task.entity'
import { TasksRepository } from './tasks.repository'
@Module({
imports: [TypeOrmModule.forFeature([Task])],
controllers: [TasksController],
providers: [TasksService, TasksRepository],
})
export class TasksModule {}10. Refactoring for Tasks Service
Array private tasks: Task[] dihapus dari service. Service sekarang menerima repository melalui dependency injection:
typescript
// src/tasks/tasks.service.ts
@Injectable()
export class TasksService {
constructor(private tasksRepository: TasksRepository) {}
async getTasks(filterDto: GetTasksFilterDto): Promise<Task[]> {
return this.tasksRepository.getTasks(filterDto)
}
async createTask(createTaskDto: CreateTaskDto): Promise<Task> {
return this.tasksRepository.createTask(createTaskDto)
}
// ...
}Perhatikan perubahan besar: semua method kini async dan mengembalikan Promise<T>. Ini karena operasi database adalah I/O asinkron — tidak bisa langsung mengembalikan nilai seperti operasi array sebelumnya.
11. Persistence: Getting a Task by ID
typescript
// src/tasks/tasks.service.ts
async getTaskById(id: string): Promise<Task> {
const found = await this.tasksRepository.findOne({ where: { id } })
if (!found) {
throw new NotFoundException(`Task dengan ID "${id}" tidak ditemukan`)
}
return found
}typescript
// src/tasks/tasks.controller.ts
@Get(':id')
async getTaskById(@Param('id') id: string): Promise<Task> {
return this.tasksService.getTaskById(id)
}Controller juga perlu async karena memanggil method async service.
12. Persistence: Creating a Task
Method createTask di repository membuat entity baru dengan this.create() lalu menyimpannya dengan this.save(). Pemisahan keduanya penting: create() hanya membuat object TypeScript, belum ke database. save() yang mengirim INSERT ke database:
typescript
// src/tasks/tasks.repository.ts
async createTask(createTaskDto: CreateTaskDto): Promise<Task> {
const { title, description } = createTaskDto
const task = this.create({ // membuat instance Task, belum ke database
title,
description,
status: TaskStatus.OPEN,
})
await this.save(task) // INSERT ke database, mengisi id secara otomatis
return task
}13. Persistence: Deleting a Task
TypeORM delete() langsung menghapus berdasarkan kondisi tanpa perlu mengambil entity lebih dulu. Ini lebih efisien karena hanya satu query ke database:
typescript
// src/tasks/tasks.service.ts
async deleteTask(id: string): Promise<void> {
const result = await this.tasksRepository.delete(id)
if (result.affected === 0) {
// tidak ada row yang terhapus — berarti ID tidak ada
throw new NotFoundException(`Task dengan ID "${id}" tidak ditemukan`)
}
}result.affected adalah jumlah baris yang terdampak operasi. Jika 0, berarti tidak ada task dengan ID tersebut.
14. Persistence: Update Task Status
Pola paling sederhana: ambil entity, ubah property, simpan kembali:
typescript
// src/tasks/tasks.service.ts
async updateTaskStatus(id: string, status: TaskStatus): Promise<Task> {
const task = await this.getTaskById(id) // akan throw 404 jika tidak ada
task.status = status
await this.tasksRepository.save(task) // UPDATE ke database
return task
}15. Small Change Needed
Method getAllTasks() dan getTasksWithFilters() disatukan menjadi satu method getTasks() agar lebih mudah diperluas dengan Query Builder. Service dan controller masing-masing disesuaikan untuk memanggil satu method ini.
16. Persistence: Getting All Tasks
Query Builder dipakai untuk membangun query yang fleksibel. Kondisi WHERE ditambahkan hanya jika parameter tersedia:
typescript
// src/tasks/tasks.repository.ts
import { GetTasksFilterDto } from './dto/get-tasks-filter.dto'
async getTasks(filterDto: GetTasksFilterDto): Promise<Task[]> {
const { status, search } = filterDto
const query = this.createQueryBuilder('task') // 'task' adalah alias tabel
if (status) {
query.andWhere('task.status = :status', { status })
}
if (search) {
query.andWhere(
'(LOWER(task.title) LIKE LOWER(:search) OR LOWER(task.description) LIKE LOWER(:search))',
{ search: `%${search}%` },
)
}
const tasks = await query.getMany()
return tasks
}Parameter binding :status dan :search (bukan string interpolasi) penting untuk mencegah SQL injection. Jangan pernah memasukkan input pengguna langsung ke string query SQL.
Konsep Kunci
| Konsep | Fungsi | Catatan |
|---|---|---|
| Entity | Representasi table database | Class TypeScript dengan decorator TypeORM |
| Repository | Layer akses data | Tempat query dan operasi persistence |
| Data Mapper | Memisahkan entity dari data access | Lebih maintainable untuk aplikasi besar |
| Query Builder | Membangun query kompleks secara programmatic | Cocok untuk filter kondisional |
| async/await | Menangani operasi database | Semua query database bersifat async |
synchronize | Menyesuaikan schema otomatis | Praktis di dev, berisiko di production |
Command dan Konfigurasi
bash
docker run --name postgres-nest -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres
yarn add @nestjs/typeorm typeorm pgtypescript
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'task_management',
autoLoadEntities: true,
synchronize: true,
})Pola Entity dan Repository
typescript
@Entity()
export class Task {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
title: string
@Column()
description: string
@Column()
status: TaskStatus
}typescript
const query = this.createQueryBuilder('task')
if (status) {
query.andWhere('task.status = :status', { status })
}
if (search) {
query.andWhere(
'(LOWER(task.title) LIKE LOWER(:search) OR LOWER(task.description) LIKE LOWER(:search))',
{ search: `%${search}%` },
)
}Jebakan Umum
- Lupa
awaitsaat memanggil repository method. - Mencampur business logic di repository dan membuat service kehilangan peran.
- Menggunakan
synchronize: truedi production. - Membuat query string manual tanpa parameter binding.
- Tidak mengecek
result.affectedsetelah delete. - Mengira ORM membuat pemahaman SQL tidak diperlukan sama sekali.
Pertanyaan Reflektif
- Mengapa database operation membuat return type berubah menjadi
Promise<T>? - Apa keuntungan Data Mapper dibanding Active Record dalam aplikasi NestJS?
- Mengapa
delete(id)bisa lebih efisien daripada mengambil entity lalu menghapusnya? - Mengapa
synchronize: trueberbahaya di production? - Bagaimana Query Builder membantu menggabungkan filter status dan search tanpa banyak method terpisah?