Appearance
Chapter 1 - Task Management REST API
Ikhtisar Chapter
Chapter ini adalah fondasi utama kursus. Anda mulai dari proyek NestJS kosong, lalu membangun REST API sederhana untuk aplikasi task management. Fokusnya bukan hanya membuat endpoint berjalan, tetapi memahami cara NestJS memisahkan tanggung jawab melalui module, controller, service, model, dan DTO.
Peta Cepat
FokusREST API, CRUD, modularitas, dependency injection
Hasil AkhirEndpoint task management dengan create, read, update, delete, search, dan filter
Bekal BerikutnyaDTO dan struktur service yang siap divalidasi dan dipindahkan ke database
Gambaran Besar
Materi dimulai dengan memperkenalkan aplikasi yang akan dibangun: task management API. Aplikasi ini memiliki resource tasks, lalu nanti akan dikembangkan dengan autentikasi, database, logging, konfigurasi, dan testing. Sejak awal, kursus menekankan bahwa NestJS cocok untuk backend yang modular dan production-oriented.
Bagian paling penting dari chapter ini adalah cara berpikirnya. Controller menangani request HTTP, service menyimpan business logic, module mengelompokkan komponen yang saling terkait, sedangkan DTO menjadi kontrak data yang masuk dari client. Dengan pembagian ini, aplikasi tetap mudah dipahami ketika fitur bertambah.
Alur Arsitektur
text
HTTP Request
|
v
Controller: memilih handler berdasarkan path dan HTTP method
|
v
DTO dan parameter decorator: mengambil body, param, atau query
|
v
Service: menjalankan business logic task
|
v
In-memory store sementara: array task sebelum masuk database
|
v
NestJS mengubah return value menjadi HTTP response JSONStruktur Direktori
Setelah chapter ini selesai, struktur proyek kamu akan terlihat seperti ini:
nestjs-task-management/
├── src/
│ ├── main.ts ← entry point, NestFactory.create()
│ ├── app.module.ts ← root module, import TasksModule
│ └── tasks/ ← semua file terkait task di sini
│ ├── dto/
│ │ ├── create-task.dto.ts ← title + description
│ │ └── get-tasks-filter.dto.ts ← status? + search?
│ ├── task.model.ts ← interface Task (belum entity)
│ ├── task-status.enum.ts ← OPEN | IN_PROGRESS | DONE
│ ├── tasks.controller.ts ← routing GET/POST/PATCH/DELETE
│ ├── tasks.module.ts ← mendaftarkan controller + service
│ └── tasks.service.ts ← business logic, array in-memory
├── test/
├── package.json
└── tsconfig.jsonPola Penamaan
Semua file NestJS mengikuti konvensi nama.tipe.ts:
tasks.controller.ts— controllertasks.service.ts— servicetasks.module.ts— modulecreate-task.dto.ts— DTOtask-status.enum.ts— enum
Ringkasan Lecture
1. Project Overview
Kursus memperkenalkan aplikasi task management sebagai proyek latihan utama. API ini akan memiliki resource tasks dengan operasi CRUD lengkap: ambil semua task, ambil satu task berdasarkan ID, buat task baru, ubah status, hapus, serta cari dan filter berdasarkan status dan keyword. Di chapter-chapter berikutnya, API ini akan diperkaya dengan autentikasi JWT, database PostgreSQL, logging, konfigurasi environment, dan unit testing.
Kursus ini bukan hanya tentang membuat endpoint bekerja, tetapi tentang memahami mengapa setiap bagian berada di tempatnya masing-masing.
2. Creating Our Project via NestJS CLI
NestJS CLI diinstal secara global dan dipakai untuk membuat proyek baru:
bash
npm install -g @nestjs/cli
nest new nestjs-task-managementCLI akan menanyakan package manager pilihan (npm atau yarn), lalu menghasilkan struktur lengkap: src/, test/, package.json, tsconfig.json, dan konfigurasi ESLint. Dengan CLI, semua setup awal yang rawan salah ketik sudah ditangani secara konsisten.
Setelah proyek dibuat, jalankan server development:
bash
cd nestjs-task-management
yarn start:devServer berjalan di http://localhost:3000 dan auto-reload setiap kali ada perubahan file.
3. NestJS Project Structure
Tiga file paling penting di awal:
src/main.ts — Entry point aplikasi. Di sinilah NestFactory.create() dipanggil untuk membuat instance aplikasi dan memulai HTTP server.
typescript
// src/main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
await app.listen(3000)
}
bootstrap()src/app.module.ts — Root module yang menjadi pusat pendaftaran semua module lain. Semua fitur masuk ke sini.
typescript
// src/app.module.ts
import { Module } from '@nestjs/common'
@Module({
imports: [],
})
export class AppModule {}File bawaan app.controller.ts, app.service.ts, dan file spec-nya bisa dihapus karena kita akan membangun struktur sendiri dari awal.
4. Introduction to NestJS Modules
Module adalah cara NestJS mengelompokkan kode yang saling berkaitan. Sebuah module memiliki empat property utama:
typescript
@Module({
imports: [], // Module lain yang dibutuhkan
controllers: [], // Controller milik module ini
providers: [], // Service dan provider milik module ini
exports: [], // Provider yang boleh dipakai module lain
})Analoginya seperti folder domain di proyek nyata: tasks/, auth/, users/ — masing-masing punya module sendiri. Pemisahan ini membuat aplikasi bisa tumbuh besar tanpa satu file menjadi tempat pembuangan semua logika.
5. Creating a Tasks Module
Module task dibuat dengan satu command:
bash
nest g module tasksCLI membuat file src/tasks/tasks.module.ts dan secara otomatis mendaftarkannya ke AppModule:
typescript
// src/tasks/tasks.module.ts
import { Module } from '@nestjs/common'
@Module({})
export class TasksModule {}typescript
// src/app.module.ts — CLI menambahkan ini otomatis
import { TasksModule } from './tasks/tasks.module'
@Module({
imports: [TasksModule],
})
export class AppModule {}Dari titik ini, semua file yang berkaitan dengan task (controller, service, entity, dto) ditempatkan di dalam folder src/tasks/.
6. Introduction to NestJS Controllers
Controller bertugas menerima request HTTP dan mengembalikan response. Setiap controller diikat ke satu base route melalui decorator @Controller(). Handler di dalamnya menggunakan decorator HTTP method seperti @Get(), @Post(), @Patch(), @Delete().
typescript
@Controller('tasks') // semua route dimulai dengan /tasks
export class TasksController {
@Get() // GET /tasks
getAllTasks() { ... }
@Get(':id') // GET /tasks/:id
getTaskById() { ... }
@Post() // POST /tasks
createTask() { ... }
@Delete(':id') // DELETE /tasks/:id
deleteTask() { ... }
@Patch(':id/status') // PATCH /tasks/:id/status
updateTaskStatus() { ... }
}Aturan utama: controller hanya bertugas mengarahkan traffic. Jangan taruh logika bisnis di sini.
7. Creating a Tasks Controller
Controller dibuat menggunakan CLI dengan flag --no-spec agar file test tidak ikut dibuat:
bash
nest g controller tasks --no-specCLI membuat src/tasks/tasks.controller.ts dan mendaftarkannya ke TasksModule secara otomatis:
typescript
// src/tasks/tasks.controller.ts
import { Controller } from '@nestjs/common'
@Controller('tasks')
export class TasksController {}typescript
// src/tasks/tasks.module.ts — diupdate CLI
@Module({
controllers: [TasksController],
})
export class TasksModule {}8. Introduction to Providers and Services
Provider adalah class yang bisa di-inject oleh NestJS melalui mekanisme dependency injection. Service adalah jenis provider paling umum — tempat logika bisnis tinggal.
Decorator @Injectable() memberi tahu NestJS bahwa class ini dikelola oleh IoC container-nya. NestJS bertanggung jawab membuat instance class tersebut, bukan kita.
typescript
@Injectable()
export class TasksService {
// logika bisnis di sini
}Mengapa ini lebih baik dari new TasksService() di controller?
- NestJS yang mengurus siklus hidup instance
- Mudah diganti dengan mock saat unit testing
- Dependency bisa punya dependency lain dan semua diurus otomatis
9. Creating a Tasks Service
bash
nest g service tasks --no-specCLI membuat src/tasks/tasks.service.ts dan mendaftarkannya ke TasksModule sebagai provider:
typescript
// src/tasks/tasks.service.ts
import { Injectable } from '@nestjs/common'
@Injectable()
export class TasksService {}typescript
// src/tasks/tasks.module.ts
@Module({
controllers: [TasksController],
providers: [TasksService], // didaftarkan otomatis
})
export class TasksModule {}Lalu service di-inject ke controller melalui constructor. Sintaks private tasksService: TasksService adalah shorthand TypeScript yang sekaligus mendeklarasikan dan mengisi property private:
typescript
// src/tasks/tasks.controller.ts
import { TasksService } from './tasks.service'
@Controller('tasks')
export class TasksController {
constructor(private tasksService: TasksService) {}
// sekarang this.tasksService tersedia di semua method
}10. Feature: Getting All Tasks
Untuk sementara, data disimpan di array in-memory di dalam service. Array dibuat private agar hanya bisa diakses melalui method service, bukan langsung dari controller.
typescript
// src/tasks/tasks.service.ts
@Injectable()
export class TasksService {
private tasks: Task[] = [] // penyimpanan sementara
getAllTasks(): Task[] {
return this.tasks
}
}Di controller, handler GET /tasks memanggil service dan NestJS otomatis mengonversi return value menjadi JSON response:
typescript
// src/tasks/tasks.controller.ts
@Get()
getAllTasks(): Task[] {
return this.tasksService.getAllTasks()
}Saat ini belum ada data, jadi response hanya array kosong []. Tapi strukturnya sudah benar: controller memanggil service, service mengembalikan data.
11. Creating a Postman Collection
Postman adalah tool untuk menguji HTTP API secara manual. Buat collection bernama "NestJS Task Management" dan tambahkan request:
GET http://localhost:3000/tasks— coba response array kosong
Simpan request ini di dalam collection agar bisa dijalankan ulang kapan saja tanpa mengetik ulang URL.
12. Defining a Task Model
Model mendefinisikan bentuk data task. Di TypeScript, model bisa berupa interface atau class. Untuk sementara, interface sudah cukup. Yang penting adalah enum TaskStatus — membatasi nilai status agar hanya bisa OPEN, IN_PROGRESS, atau DONE.
typescript
// src/tasks/task.model.ts
export enum TaskStatus {
OPEN = 'OPEN',
IN_PROGRESS = 'IN_PROGRESS',
DONE = 'DONE',
}
export interface Task {
id: string
title: string
description: string
status: TaskStatus
}Dengan enum ini, TypeScript akan menolak assignment seperti status = 'COMPLETE' saat compile, jauh sebelum request masuk ke aplikasi.
13–14. Creating a Task (Controller dan Service)
Fitur create task melibatkan keduanya. Di controller, @Body() dipakai untuk mengambil seluruh request body. Di service, UUID baru dibuat, status default dipasang, lalu task disimpan ke array.
Controller — menerima request:
typescript
// src/tasks/tasks.controller.ts
@Post()
createTask(
@Body('title') title: string,
@Body('description') description: string,
): Task {
return this.tasksService.createTask(title, description)
}Service — memproses dan menyimpan:
typescript
// src/tasks/tasks.service.ts
import { v4 as uuid } from 'uuid'
createTask(title: string, description: string): Task {
const task: Task = {
id: uuid(), // ID unik dibuat di sini, bukan dari client
title,
description,
status: TaskStatus.OPEN, // status default selalu OPEN
}
this.tasks.push(task)
return task // dikembalikan sebagai respons JSON
}Perhatikan bahwa id dan status dibuat oleh server — bukan terima dari client. Inilah mengapa nanti kita perlu DTO yang terpisah dari model lengkap Task.
15. Intro to Data Transfer Objects (DTO)
Pendekatan di lecture 13–14 mengambil title dan description secara terpisah. Ini menjadi masalah ketika field bertambah — constructor harus diubah di mana-mana. DTO menyelesaikan ini dengan membungkus semua field dalam satu object.
Perbandingan:
typescript
// Tanpa DTO — parameter terpisah, rawan perubahan
createTask(title: string, description: string): Task { ... }
// Dengan DTO — satu object, mudah diperluas
createTask(createTaskDto: CreateTaskDto): Task { ... }DTO adalah class (bukan interface) karena class bisa diberi decorator validasi di chapter berikutnya.
16. Implementing CreateTaskDto
typescript
// src/tasks/dto/create-task.dto.ts
export class CreateTaskDto {
title: string
description: string
}Controller dan service diupdate untuk memakai DTO:
typescript
// src/tasks/tasks.controller.ts
@Post()
createTask(@Body() createTaskDto: CreateTaskDto): Task {
return this.tasksService.createTask(createTaskDto)
}typescript
// src/tasks/tasks.service.ts
createTask(createTaskDto: CreateTaskDto): Task {
const { title, description } = createTaskDto // destructuring
const task: Task = {
id: uuid(),
title,
description,
status: TaskStatus.OPEN,
}
this.tasks.push(task)
return task
}17. Getting a Task by ID
Path parameter :id diambil menggunakan decorator @Param('id'). Service mencari task dengan Array.find():
typescript
// src/tasks/tasks.controller.ts
@Get(':id')
getTaskById(@Param('id') id: string): Task {
return this.tasksService.getTaskById(id)
}typescript
// src/tasks/tasks.service.ts
getTaskById(id: string): Task {
const found = this.tasks.find((task) => task.id === id)
// belum ada penanganan jika tidak ditemukan — akan diperbaiki di ch.2
return found
}18. Deleting a Task
Array.filter() dipakai karena ia mengembalikan array baru, sedangkan Array.splice() mutasi array asli — memakai filter lebih aman dan lebih idiomatis:
typescript
// src/tasks/tasks.controller.ts
@Delete(':id')
deleteTask(@Param('id') id: string): void {
this.tasksService.deleteTask(id)
}typescript
// src/tasks/tasks.service.ts
deleteTask(id: string): void {
this.tasks = this.tasks.filter((task) => task.id !== id)
}Delete berhasil tidak mengembalikan body apa pun — cukup HTTP 200 atau 204. Ini sesuai konvensi REST.
19. Update Task Status
PATCH dipakai (bukan PUT) karena kita hanya mengubah satu field, bukan mengganti seluruh resource:
typescript
// src/tasks/tasks.controller.ts
@Patch(':id/status')
updateTaskStatus(
@Param('id') id: string,
@Body('status') status: TaskStatus,
): Task {
return this.tasksService.updateTaskStatus(id, status)
}typescript
// src/tasks/tasks.service.ts
updateTaskStatus(id: string, status: TaskStatus): Task {
const task = this.getTaskById(id) // reuse method yang sama
task.status = status
return task
}Dengan memanfaatkan getTaskById(), error handling "task tidak ditemukan" cukup ditangani di satu tempat saja.
20. Searching and Filtering
Filter menggunakan query string: GET /tasks?status=OPEN&search=clean. Query diambil dengan @Query() dan dibuatkan DTO-nya sendiri karena field ini opsional.
typescript
// src/tasks/dto/get-tasks-filter.dto.ts
export class GetTasksFilterDto {
status?: TaskStatus
search?: string
}typescript
// src/tasks/tasks.controller.ts
@Get()
getTasks(@Query() filterDto: GetTasksFilterDto): Task[] {
if (Object.keys(filterDto).length > 0) {
return this.tasksService.getTasksWithFilters(filterDto)
}
return this.tasksService.getAllTasks()
}typescript
// src/tasks/tasks.service.ts
getTasksWithFilters(filterDto: GetTasksFilterDto): Task[] {
const { status, search } = filterDto
let tasks = this.getAllTasks()
if (status) {
tasks = tasks.filter((task) => task.status === status)
}
if (search) {
tasks = tasks.filter(
(task) =>
task.title.toLowerCase().includes(search.toLowerCase()) ||
task.description.toLowerCase().includes(search.toLowerCase()),
)
}
return tasks
}Konsep Kunci
| Konsep | Inti Pemahaman | Kapan Dipakai |
|---|---|---|
| Module | Mengelompokkan fitur dan dependency | Saat memisahkan domain seperti tasks dan auth |
| Controller | Menerima request HTTP | Saat membuat endpoint REST |
| Service | Menjalankan business logic | Saat operasi tidak sekadar mengambil parameter |
| Provider | Dependency yang bisa di-inject | Saat class dibutuhkan oleh class lain |
| Dependency Injection | NestJS membuat dan menyuntikkan instance | Saat controller membutuhkan service |
| DTO | Kontrak data antar layer atau request | Saat menerima body atau query yang terstruktur |
| Enum | Daftar nilai yang dibatasi | Saat status hanya boleh nilai tertentu |
Endpoint yang Dibangun
| Method | Route | Tujuan |
|---|---|---|
GET | /tasks | Mengambil semua task, dengan filter opsional |
POST | /tasks | Membuat task baru |
GET | /tasks/:id | Mengambil satu task berdasarkan ID |
PATCH | /tasks/:id/status | Mengubah status task |
DELETE | /tasks/:id | Menghapus task |
Pola Kode Penting
typescript
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
getTasks(@Query() filterDto: GetTasksFilterDto): Task[] {
return this.tasksService.getTasks(filterDto)
}
}typescript
@Injectable()
export class TasksService {
private tasks: Task[] = []
getAllTasks(): Task[] {
return this.tasks
}
}Jebakan Umum
- Menaruh semua logika di controller membuat kode cepat penuh dan sulit dites.
- Membiarkan array task public membuka peluang perubahan data dari luar service.
- Menggunakan model lengkap sebagai input create dapat memaksa client mengirim field yang seharusnya dibuat server.
- Lupa menyamakan nama path parameter, misalnya
@Get(':id')tetapi mengambil@Param('taskId'). - Menganggap in-memory store cukup untuk aplikasi nyata. Chapter ini hanya tahap latihan sebelum masuk database.
Pertanyaan Reflektif
- Mengapa
TasksModulelebih baik dipisahkan daripada semua logic diletakkan diAppModule? - Apa perbedaan tugas controller dan service dalam endpoint
POST /tasks? - Mengapa
CreateTaskDtotidak perlu memiliki fieldiddanstatus? - Kapan path parameter lebih tepat daripada query parameter?
- Apa keuntungan membuat method service seperti
getTaskById()lalu memakainya ulang di update status?