Skip to content

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 JSON

Struktur 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.json

Pola Penamaan

Semua file NestJS mengikuti konvensi nama.tipe.ts:

  • tasks.controller.ts — controller
  • tasks.service.ts — service
  • tasks.module.ts — module
  • create-task.dto.ts — DTO
  • task-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-management

CLI 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:dev

Server 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 tasks

CLI 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-spec

CLI 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-spec

CLI 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

KonsepInti PemahamanKapan Dipakai
ModuleMengelompokkan fitur dan dependencySaat memisahkan domain seperti tasks dan auth
ControllerMenerima request HTTPSaat membuat endpoint REST
ServiceMenjalankan business logicSaat operasi tidak sekadar mengambil parameter
ProviderDependency yang bisa di-injectSaat class dibutuhkan oleh class lain
Dependency InjectionNestJS membuat dan menyuntikkan instanceSaat controller membutuhkan service
DTOKontrak data antar layer atau requestSaat menerima body atau query yang terstruktur
EnumDaftar nilai yang dibatasiSaat status hanya boleh nilai tertentu

Endpoint yang Dibangun

MethodRouteTujuan
GET/tasksMengambil semua task, dengan filter opsional
POST/tasksMembuat task baru
GET/tasks/:idMengambil satu task berdasarkan ID
PATCH/tasks/:id/statusMengubah status task
DELETE/tasks/:idMenghapus 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

  1. Mengapa TasksModule lebih baik dipisahkan daripada semua logic diletakkan di AppModule?
  2. Apa perbedaan tugas controller dan service dalam endpoint POST /tasks?
  3. Mengapa CreateTaskDto tidak perlu memiliki field id dan status?
  4. Kapan path parameter lebih tepat daripada query parameter?
  5. Apa keuntungan membuat method service seperti getTaskById() lalu memakainya ulang di update status?