NESTJS NODE.JS BACKEND TYPESCRIPT FRAMEWORK ARCHITECTURE MICROSERVICES DEPENDENCY-INJECTION WEB-DEVELOPMENT API-DEVELOPMENT SCALABILITY MAINTAINABILITY TESTING DEVOPS

Membangun Backend Skalabel dan Maintainable dengan NestJS: Arsitektur Modern untuk Node.js

⏱️ 10 menit baca
👨‍💻

Membangun Backend Skalabel dan Maintainable dengan NestJS: Arsitektur Modern untuk Node.js

1. Pendahuluan

Sebagai developer Node.js, kita sering kali dihadapkan pada dilema: bagaimana membangun aplikasi backend yang tidak hanya cepat dalam pengembangan, tetapi juga skalabel, mudah di-maintain, dan tahan uji seiring berjalannya waktu? Ekosistem Node.js yang sangat fleksibel, meskipun menjadi kekuatan, kadang juga menjadi kelemahan. Tanpa struktur yang jelas, proyek Node.js bisa dengan cepat berubah menjadi “spaghetti code” yang sulit dipahami dan dikembangkan.

Di sinilah NestJS hadir sebagai solusi. NestJS adalah framework progresif Node.js untuk membangun aplikasi server-side yang efisien dan skalabel. Ia menggunakan TypeScript secara out-of-the-box dan menggabungkan elemen-elemen dari OOP (Object-Oriented Programming), FP (Functional Programming), dan FRP (Functional Reactive Programming). NestJS terinspirasi oleh Angular, sehingga developer yang familiar dengan Angular akan merasa betah dengan struktur dan konsepnya.

Artikel ini akan membawa Anda menyelami NestJS, memahami filosofi arsitekturnya, dan melihat bagaimana ia dapat membantu Anda membangun backend Node.js yang lebih kokoh, terstruktur, dan siap untuk menghadapi tantangan skala besar.

2. Apa Itu NestJS dan Mengapa Menggunakannya?

NestJS bukan sekadar framework HTTP seperti Express atau Fastify (meskipun ia menggunakan keduanya di bawah kap mesin). NestJS adalah framework arsitektural yang menyediakan struktur yang kuat, pola desain yang sudah terbukti, dan alat-alat untuk membangun aplikasi yang kompleks.

Mengapa NestJS?

📌 Singkatnya, NestJS membantu Anda membangun backend yang terasa seperti “enterprise-grade” tanpa kehilangan kecepatan pengembangan Node.js.

3. Pondasi Arsitektur NestJS: Modul, Controller, dan Provider

Mari kita bedah tiga pilar utama arsitektur NestJS:

3.1. Modul (Modules)

Modul adalah unit organisasi fundamental dalam NestJS. Setiap aplikasi NestJS setidaknya memiliki satu root module (biasanya AppModule). Modul mengelompokkan controller, provider, dan modul lainnya yang terkait secara fungsional.

💡 Analogi: Bayangkan aplikasi Anda sebagai sebuah kota. Modul adalah distrik atau lingkungan di kota tersebut (misalnya, “Distrik Pengguna,” “Distrik Produk,” “Distrik Otentikasi”). Setiap distrik memiliki fungsi spesifik dan berisi bangunan-bangunan (controller, service) yang melayani fungsi tersebut.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module'; // Import UsersModule

@Module({
  imports: [UsersModule], // Daftarkan modul lain yang digunakan
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

3.2. Controller (Controllers)

Controller bertanggung jawab untuk menangani request HTTP yang masuk dan mengembalikan respons ke klien. Mereka mendefinisikan endpoint API dan logika dasar untuk merespons permintaan tersebut.

// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users') // Base path untuk endpoint ini
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id); // Konversi string id ke number
  }
}

3.3. Provider (Providers)

Provider adalah kelas yang bisa di-inject ke kelas lain. Ini termasuk service, repository, factory, helper, dll. Provider adalah tempat Anda menempatkan sebagian besar logika bisnis aplikasi Anda.

💡 Analogi: Jika controller adalah resepsionis yang menerima permintaan, maka provider adalah para ahli di belakang layar (misalnya, akuntan, insinyur, koki) yang melakukan pekerjaan sebenarnya untuk memenuhi permintaan tersebut.

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity'; // Misalkan ada entity User

@Injectable() // Menandakan kelas ini bisa di-inject
export class UsersService {
  private users: User[] = []; // Contoh data dummy

  create(createUserDto: CreateUserDto): User {
    const newUser: User = { id: this.users.length + 1, ...createUserDto };
    this.users.push(newUser);
    return newUser;
  }

  findAll(): User[] {
    return this.users;
  }

  findOne(id: number): User | undefined {
    return this.users.find(user => user.id === id);
  }
}

4. Dependency Injection dalam Praktik

Salah satu fitur paling powerful di NestJS adalah implementasi Dependency Injection (DI) yang ekstensif. DI adalah pola desain di mana komponen menerima dependensinya dari luar, alih-alih membuatnya sendiri. Ini membuat kode lebih modular, mudah diuji, dan fleksibel.

Dalam NestJS, DI bekerja secara otomatis. Ketika Anda mendeklarasikan sebuah Provider (seperti UsersService) dan kemudian meng-inject-nya ke dalam Controller atau Service lain melalui constructor, NestJS akan secara otomatis membuat instance dari Provider tersebut dan menyediakannya.

// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service'; // AppService adalah provider

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {} // DI terjadi di sini

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

✅ Manfaat utama DI adalah testability. Anda dapat dengan mudah mengganti dependensi asli dengan mock atau stub saat melakukan unit testing, memastikan bahwa Anda hanya menguji satu unit kode pada satu waktu.

5. Menggali Lebih Dalam: Guards, Interceptors, Pipes, dan Filters

NestJS menyediakan serangkaian fitur “enhancer” yang memungkinkan Anda menambahkan logika di berbagai tahap siklus request-response tanpa mengubah logika inti controller/service Anda.

5.1. Guards (Penjaga)

Guards adalah kelas yang menentukan apakah request tertentu harus diproses oleh route handler atau tidak. Mereka sangat berguna untuk implementasi autentikasi dan otorisasi.

// src/auth/guards/auth.guard.ts (contoh sederhana)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    // Logika untuk memverifikasi token atau sesi
    return request.headers.authorization === 'Bearer valid-token';
  }
}

// Penggunaan di controller
// @UseGuards(AuthGuard)
// @Get('protected')
// getProtectedResource() { ... }

5.2. Interceptors (Penyela)

Interceptors memungkinkan Anda untuk “menyela” eksekusi request/response. Mereka bisa digunakan untuk:

// src/common/interceptors/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

// Penggunaan di controller
// @UseInterceptors(LoggingInterceptor)
// @Get()
// findAll() { ... }

5.3. Pipes (Pipa)

Pipes digunakan untuk transformasi data (misalnya, mengubah string menjadi integer) atau validasi data (misalnya, memastikan parameter adalah UUID yang valid). Mereka dieksekusi sebelum controller method dipanggil.

// src/common/pipes/parse-int.pipe.ts (NestJS memiliki built-in ParseIntPipe)
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed: id must be an integer');
    }
    return val;
  }
}

// Penggunaan di controller
// @Get(':id')
// findOne(@Param('id', ParseIntPipe) id: number) { ... }

5.4. Exception Filters (Filter Pengecualian)

Exception Filters memungkinkan Anda untuk mengontrol sepenuhnya lapisan pengecualian (exception layer) aplikasi Anda. Anda dapat menangkap pengecualian yang tidak tertangani dan mengirimkan respons HTTP yang disesuaikan.

// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message: exception.message,
      });
  }
}

// Penggunaan di controller (atau global)
// @UseFilters(HttpExceptionFilter)
// @Get('error')
// throwError() {
//   throw new HttpException('Something went wrong', HttpStatus.BAD_REQUEST);
// }

🎯 Dengan kombinasi enhancers ini, Anda dapat membangun pipeline request yang sangat fleksibel dan kuat, memisahkan kekhawatiran lintas-pemotongan (cross-cutting concerns) dari logika bisnis utama.

6. NestJS untuk Microservices dan Event-Driven Architecture

NestJS sangat cocok untuk membangun microservices. Konsep modularnya secara alami mendukung pemisahan aplikasi menjadi layanan-layanan yang lebih kecil dan mandiri. NestJS memiliki dukungan bawaan untuk berbagai pola komunikasi microservice:

Dengan NestJS, Anda dapat dengan mudah mendefinisikan client dan server microservice, serta message pattern untuk komunikasi antar layanan.

// Contoh konfigurasi microservice server (menggunakan TCP)
// src/main.ts (untuk microservice-A)
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice(AppModule, {
    transport: Transport.TCP,
    options: { port: 3001 },
  });
  await app.listen();
  console.log('Microservice-A is listening');
}
bootstrap();
// Contoh konfigurasi microservice client (dari microservice-B)
// src/users/users.module.ts (di microservice-B)
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'USER_SERVICE',
        transport: Transport.TCP,
        options: { port: 3001 },
      },
    ]),
  ],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

// Penggunaan di UsersService (di microservice-B)
// import { Inject, Injectable } from '@nestjs/common';
// import { ClientProxy } from '@nestjs/microservices';
// import { Observable } from 'rxjs';

// @Injectable()
// export class UsersService {
//   constructor(@Inject('USER_SERVICE') private client: ClientProxy) {}

//   getUsers(): Observable<string> {
//     return this.client.send<string>({ cmd: 'get_users' }, '');
//   }
// }

Ini membuka pintu untuk arsitektur yang sangat skalabel dan fault-tolerant, di mana setiap layanan dapat dikembangkan, di-deploy, dan di-scale secara independen.

Kesimpulan

NestJS menawarkan pendekatan yang menyegarkan untuk pengembangan backend Node.js. Dengan mengadopsi prinsip-prinsip desain yang solid seperti modularitas, Dependency Injection, dan Type-Safety melalui TypeScript, NestJS membantu developer membangun aplikasi yang tidak hanya efisien tetapi juga mudah dikelola, diuji, dan diskalakan.

Jika Anda mencari cara untuk membawa proyek Node.js Anda ke level berikutnya, beralih dari “spaghetti code” ke arsitektur yang terstruktur dan modern, NestJS adalah pilihan yang patut dipertimbangkan. Ini adalah investasi waktu yang akan terbayar lunas dalam jangka panjang, terutama untuk aplikasi yang kompleks dan berbasis tim.

Mulai eksplorasi Anda dengan NestJS CLI dan rasakan sendiri bagaimana ia bisa mengubah cara Anda membangun backend Node.js!

🔗 Baca Juga