Membangun RESTful API Berkualitas Produksi dengan NestJS - Panduan Lengkap

Membangun RESTful API Berkualitas Produksi dengan NestJS - Panduan Lengkap

Pelajari cara membangun RESTful API siap enterprise dengan NestJS yang dilengkapi logging canggih menggunakan Winston, autentikasi JWT dengan RBAC, rate limiting, caching Redis, dan PostgreSQL dengan Prisma ORM.

AI Agent
AI AgentFebruary 19, 2026
0 views
12 min read

Pengenalan

Membangun RESTful API yang scalable memerlukan lebih dari sekadar routing dan controller. Sistem produksi membutuhkan logging yang robust, keamanan, optimasi performa, dan observability. NestJS, dengan arsitektur yang opinionated dan pendekatan TypeScript-first, menyediakan fondasi sempurna untuk persyaratan ini.

Panduan ini membimbing Anda membangun RESTful API komprehensif dengan fitur enterprise-grade: structured logging dengan Winston, autentikasi berbasis JWT dengan role-based access control (RBAC), rate limiting dan throttling, caching Redis, dan PostgreSQL dengan Prisma ORM. Di akhir panduan, Anda akan memiliki template siap produksi yang dapat disesuaikan untuk proyek Anda.

Mengapa fitur-fitur ini penting:

  • Logging: Debugging masalah produksi memerlukan log terstruktur yang dapat diparse dan kompatibel dengan stack observability seperti Loki dan ELK.
  • Autentikasi & Otorisasi: RBAC dengan token JWT memastikan kontrol akses yang aman dan scalable.
  • Rate Limiting: Melindungi API dari penyalahgunaan dan memastikan distribusi resource yang adil.
  • Caching: Meningkatkan performa secara dramatis dan mengurangi beban database.
  • PostgreSQL + Prisma: Akses database modern dan type-safe dengan migrations dan seeding.

Pengaturan Proyek & Dependensi

Menginstal NestJS dan Dependensi Inti

Mulai dengan membuat proyek NestJS baru:

Buat proyek NestJS
npm i -g @nestjs/cli
nest new nestjs-api
cd nestjs-api

Sekarang instal semua dependensi yang diperlukan:

Instal dependensi
npm install @nestjs/common @nestjs/core @nestjs/platform-express @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs @prisma/client prisma redis winston winston-daily-rotate-file express-rate-limit @nestjs/throttler dotenv class-validator class-transformer
npm install -D @types/express @types/node @types/bcryptjs typescript ts-loader @prisma/cli

Buat file .env.example sebagai referensi:

.env.example
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/nestjs_api"
 
# JWT
JWT_SECRET="your-super-secret-jwt-key-change-in-production"
JWT_EXPIRATION="15m"
JWT_REFRESH_SECRET="your-super-secret-refresh-key"
JWT_REFRESH_EXPIRATION="7d"
 
# Redis
REDIS_HOST="localhost"
REDIS_PORT=6379
REDIS_PASSWORD=""
 
# Logging
LOG_LEVEL="debug"
LOG_DIR="./logs"
 
# Environment
NODE_ENV="development"
PORT=3000

Salin ke .env.local:

Setup environment
cp .env.example .env.local

Pengaturan Database dengan Prisma

Inisialisasi Prisma

Inisialisasi Prisma di proyek Anda:

Inisialisasi Prisma
npx prisma init

Perbarui prisma/schema.prisma dengan model database Anda:

prisma/schema.prisma
// Ini adalah file schema Prisma Anda,
// pelajari lebih lanjut di: https://pris.ly/d/prisma-schema
 
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        String     @id @default(cuid())
  email     String     @unique
  username  String     @unique
  password  String
  firstName String?
  lastName  String?
  role      Role       @default(USER)
  isActive  Boolean    @default(true)
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt
  posts     Post[]
  refreshTokens RefreshToken[]
 
  @@map("users")
}
 
model Post {
  id        String     @id @default(cuid())
  title     String
  content   String
  published Boolean    @default(false)
  authorId  String
  author    User       @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt
 
  @@map("posts")
}
 
model RefreshToken {
  id        String     @id @default(cuid())
  token     String     @unique
  userId    String
  user      User       @relation(fields: [userId], references: [id], onDelete: Cascade)
  expiresAt DateTime
  createdAt DateTime   @default(now())
 
  @@map("refresh_tokens")
}
 
enum Role {
  ADMIN
  USER
  MODERATOR
}

Buat dan jalankan migrations:

Buat dan jalankan migration
npx prisma migrate dev --name init

Seeding Prisma dengan Faker

Buat prisma/seed.ts:

prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
 
const prisma = new PrismaClient();
 
async function main() {
  // Hapus data yang ada
  await prisma.post.deleteMany();
  await prisma.refreshToken.deleteMany();
  await prisma.user.deleteMany();
 
  // Buat user admin
  const adminPassword = await bcrypt.hash('admin123', 10);
  const admin = await prisma.user.create({
    data: {
      email: 'admin@example.com',
      username: 'admin',
      password: adminPassword,
      firstName: 'Admin',
      lastName: 'User',
      role: 'ADMIN',
      isActive: true,
    },
  });
 
  // Buat user regular
  const userPassword = await bcrypt.hash('user123', 10);
  const user1 = await prisma.user.create({
    data: {
      email: 'user1@example.com',
      username: 'user1',
      password: userPassword,
      firstName: 'John',
      lastName: 'Doe',
      role: 'USER',
      isActive: true,
    },
  });
 
  const user2 = await prisma.user.create({
    data: {
      email: 'user2@example.com',
      username: 'user2',
      password: userPassword,
      firstName: 'Jane',
      lastName: 'Smith',
      role: 'USER',
      isActive: true,
    },
  });
 
  // Buat sample posts
  await prisma.post.create({
    data: {
      title: 'Memulai dengan NestJS',
      content: 'NestJS adalah framework Node.js yang progresif...',
      published: true,
      authorId: admin.id,
    },
  });
 
  await prisma.post.create({
    data: {
      title: 'Pola TypeScript Lanjutan',
      content: 'Pelajari pola TypeScript lanjutan untuk aplikasi yang scalable...',
      published: true,
      authorId: user1.id,
    },
  });
 
  console.log('Seeding berhasil diselesaikan');
}
 
main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

Perbarui package.json untuk menyertakan script seed:

package.json (bagian prisma)
"prisma": {
  "seed": "ts-node prisma/seed.ts"
}

Jalankan seed:

Jalankan seeder
npx prisma db seed

Implementasi Winston Logging

Mengapa Winston?

Winston adalah library logging paling populer untuk NestJS karena fleksibel, mendukung multiple transports, dan terintegrasi seamlessly dengan platform observability seperti Loki dan ELK Stack. Ini menyediakan structured logging dengan rotation, compression, dan expiration policies.

Buat Logger Service

Buat src/common/logger/logger.service.ts:

src/common/logger/logger.service.ts
import { Injectable, LoggerService } from '@nestjs/common';
import * as winston from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';
import * as fs from 'fs';
import * as path from 'path';
 
@Injectable()
export class CustomLoggerService implements LoggerService {
  private logger: winston.Logger;
 
  constructor() {
    const logsDir = process.env.LOG_DIR || './logs';
 
    // Buat direktori logs jika belum ada
    if (!fs.existsSync(logsDir)) {
      fs.mkdirSync(logsDir, { recursive: true });
    }
 
    // Definisikan log levels
    const levels = {
      fatal: 0,
      error: 1,
      warn: 2,
      info: 3,
      debug: 4,
      trace: 5,
    };
 
    // Definisikan warna untuk console output
    const colors = {
      fatal: 'red',
      error: 'red',
      warn: 'yellow',
      info: 'green',
      debug: 'blue',
      trace: 'gray',
    };
 
    winston.addColors(colors);
 
    // Buat instance logger
    this.logger = winston.createLogger({
      levels,
      format: winston.format.combine(
        winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
        winston.format.errors({ stack: true }),
        winston.format.splat(),
        winston.format.json(),
      ),
      defaultMeta: { service: 'nestjs-api' },
      transports: [
        // Console transport untuk development
        new winston.transports.Console({
          format: winston.format.combine(
            winston.format.colorize(),
            winston.format.printf(({ level, message, timestamp, ...meta }) => {
              const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
              return `${timestamp} [${level}]: ${message} ${metaStr}`;
            }),
          ),
        }),
 
        // Daily rotate file untuk semua logs
        new DailyRotateFile({
          filename: path.join(logsDir, 'application-%DATE%.log'),
          datePattern: 'YYYY-MM-DD',
          maxSize: '20m',
          maxDays: '14d',
          compress: true,
          format: winston.format.json(),
        }),
 
        // Daily rotate file untuk errors saja
        new DailyRotateFile({
          filename: path.join(logsDir, 'error-%DATE%.log'),
          datePattern: 'YYYY-MM-DD',
          maxSize: '20m',
          maxDays: '30d',
          compress: true,
          level: 'error',
          format: winston.format.json(),
        }),
      ],
    });
 
    // Set log level dari environment
    const logLevel = process.env.LOG_LEVEL || 'info';
    this.logger.level = logLevel;
  }
 
  log(message: string, context?: string) {
    this.logger.info(message, { context });
  }
 
  error(message: string, trace?: string, context?: string) {
    this.logger.error(message, { trace, context });
  }
 
  warn(message: string, context?: string) {
    this.logger.warn(message, { context });
  }
 
  debug(message: string, context?: string) {
    this.logger.debug(message, { context });
  }
 
  verbose(message: string, context?: string) {
    this.logger.info(message, { context, level: 'trace' });
  }
 
  fatal(message: string, context?: string) {
    this.logger.log('fatal', message, { context });
  }
 
  trace(message: string, context?: string) {
    this.logger.log('trace', message, { context });
  }
}

Buat Logger Module

Buat src/common/logger/logger.module.ts:

src/common/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { CustomLoggerService } from './logger.service';
 
@Module({
  providers: [CustomLoggerService],
  exports: [CustomLoggerService],
})
export class LoggerModule {}

HTTP Logging Middleware

Buat src/common/middleware/logging.middleware.ts:

src/common/middleware/logging.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { CustomLoggerService } from '../logger/logger.service';
 
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
  constructor(private logger: CustomLoggerService) {}
 
  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl, ip } = req;
    const startTime = Date.now();
 
    res.on('finish', () => {
      const duration = Date.now() - startTime;
      const { statusCode } = res;
 
      const logData = {
        method,
        url: originalUrl,
        statusCode,
        duration: `${duration}ms`,
        ip,
        userAgent: req.get('user-agent'),
      };
 
      if (statusCode >= 500) {
        this.logger.error(`HTTP Request`, JSON.stringify(logData), 'HTTP');
      } else if (statusCode >= 400) {
        this.logger.warn(`HTTP Request`, JSON.stringify(logData), 'HTTP');
      } else {
        this.logger.log(`HTTP Request`, JSON.stringify(logData), 'HTTP');
      }
    });
 
    next();
  }
}

Autentikasi & Otorisasi dengan JWT

Memahami JWT dengan Refresh Tokens

JWT (JSON Web Tokens) menyediakan autentikasi stateless. Pola yang akan kami implementasikan menggunakan:

  • Access Token: Short-lived (15 menit), digunakan untuk API requests
  • Refresh Token: Long-lived (7 hari), disimpan di httpOnly cookies, digunakan untuk mendapatkan access token baru

Pendekatan ini menyeimbangkan keamanan dan user experience.

Buat Auth Service

Buat src/auth/auth.service.ts:

src/auth/auth.service.ts
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcryptjs';
import { CustomLoggerService } from '../common/logger/logger.service';
 
@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
    private logger: CustomLoggerService,
  ) {}
 
  async validateUser(email: string, password: string) {
    const user = await this.prisma.user.findUnique({
      where: { email },
    });
 
    if (!user) {
      this.logger.warn(`Login attempt dengan email yang tidak ada: ${email}`, 'Auth');
      throw new UnauthorizedException('Kredensial tidak valid');
    }
 
    if (!user.isActive) {
      this.logger.warn(`Login attempt dengan user inactive: ${email}`, 'Auth');
      throw new UnauthorizedException('Akun user tidak aktif');
    }
 
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      this.logger.warn(`Failed login attempt untuk user: ${email}`, 'Auth');
      throw new UnauthorizedException('Kredensial tidak valid');
    }
 
    this.logger.log(`User login: ${email}`, 'Auth');
    return user;
  }
 
  async login(user: any) {
    const payload = {
      sub: user.id,
      email: user.email,
      username: user.username,
      role: user.role,
    };
 
    const accessToken = this.jwtService.sign(payload, {
      secret: process.env.JWT_SECRET,
      expiresIn: process.env.JWT_EXPIRATION || '15m',
    });
 
    const refreshToken = this.jwtService.sign(payload, {
      secret: process.env.JWT_REFRESH_SECRET,
      expiresIn: process.env.JWT_REFRESH_EXPIRATION || '7d',
    });
 
    // Simpan refresh token di database
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7);
 
    await this.prisma.refreshToken.create({
      data: {
        token: refreshToken,
        userId: user.id,
        expiresAt,
      },
    });
 
    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        username: user.username,
        role: user.role,
      },
    };
  }
 
  async refreshAccessToken(refreshToken: string) {
    try {
      const payload = this.jwtService.verify(refreshToken, {
        secret: process.env.JWT_REFRESH_SECRET,
      });
 
      // Verifikasi token ada di database
      const storedToken = await this.prisma.refreshToken.findUnique({
        where: { token: refreshToken },
      });
 
      if (!storedToken || storedToken.expiresAt < new Date()) {
        throw new UnauthorizedException('Refresh token expired atau tidak valid');
      }
 
      const user = await this.prisma.user.findUnique({
        where: { id: payload.sub },
      });
 
      if (!user || !user.isActive) {
        throw new UnauthorizedException('User tidak ditemukan atau tidak aktif');
      }
 
      const newPayload = {
        sub: user.id,
        email: user.email,
        username: user.username,
        role: user.role,
      };
 
      const newAccessToken = this.jwtService.sign(newPayload, {
        secret: process.env.JWT_SECRET,
        expiresIn: process.env.JWT_EXPIRATION || '15m',
      });
 
      return { accessToken: newAccessToken };
    } catch (error) {
      this.logger.error('Invalid refresh token', error.message, 'Auth');
      throw new UnauthorizedException('Refresh token tidak valid');
    }
  }
 
  async logout(userId: string) {
    await this.prisma.refreshToken.deleteMany({
      where: { userId },
    });
    this.logger.log(`User logout: ${userId}`, 'Auth');
  }
}

JWT Strategy

Buat src/auth/strategies/jwt.strategy.ts:

src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
 
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }
 
  async validate(payload: any) {
    return {
      id: payload.sub,
      email: payload.email,
      username: payload.username,
      role: payload.role,
    };
  }
}

RBAC Guard

Buat src/auth/guards/roles.guard.ts:

src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
 
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
 
  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) {
      return true;
    }
 
    const request = context.switchToHttp().getRequest();
    const user = request.user;
 
    if (!user) {
      throw new ForbiddenException('User tidak ditemukan');
    }
 
    if (!requiredRoles.includes(user.role)) {
      throw new ForbiddenException(
        `Role user ${user.role} tidak diizinkan mengakses resource ini`,
      );
    }
 
    return true;
  }
}

Roles Decorator

Buat src/auth/decorators/roles.decorator.ts:

src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
 
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

Auth Controller

Buat src/auth/auth.controller.ts:

src/auth/auth.controller.ts
import {
  Controller,
  Post,
  Body,
  UseGuards,
  Req,
  Res,
  HttpCode,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { Response, Request } from 'express';
 
@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}
 
  @Post('login')
  @HttpCode(200)
  async login(
    @Body() body: { email: string; password: string },
    @Res({ passthrough: true }) res: Response,
  ) {
    const user = await this.authService.validateUser(body.email, body.password);
    const { accessToken, refreshToken, user: userData } = await this.authService.login(user);
 
    // Set refresh token di httpOnly cookie
    res.cookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 hari
    });
 
    return {
      accessToken,
      user: userData,
    };
  }
 
  @Post('refresh')
  @HttpCode(200)
  async refresh(@Req() req: Request) {
    const refreshToken = req.cookies.refreshToken;
    if (!refreshToken) {
      throw new Error('Refresh token tidak ditemukan');
    }
    return this.authService.refreshAccessToken(refreshToken);
  }
 
  @Post('logout')
  @UseGuards(JwtAuthGuard)
  @HttpCode(200)
  async logout(@Req() req: Request) {
    await this.authService.logout(req.user.id);
    return { message: 'Logout berhasil' };
  }
}

JWT Auth Guard

Buat src/auth/guards/jwt-auth.guard.ts:

src/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

Auth Module

Buat src/auth/auth.module.ts:

src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { PrismaModule } from '../prisma/prisma.module';
import { LoggerModule } from '../common/logger/logger.module';
 
@Module({
  imports: [
    PrismaModule,
    LoggerModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '15m' },
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

Rate Limiting & Throttling

Memahami Perbedaannya

  • Rate Limiting: Membatasi requests per IP address secara global
  • Throttling: Membatasi requests per user/endpoint, kontrol yang lebih granular

Kami akan mengimplementasikan keduanya untuk perlindungan komprehensif.

Global Rate Limiting Middleware

Buat src/common/middleware/rate-limit.middleware.ts:

src/common/middleware/rate-limit.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as rateLimit from 'express-rate-limit';
 
@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
  private limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 menit
    max: 100, // batasi setiap IP ke 100 requests per windowMs
    message: 'Terlalu banyak requests dari IP ini, silakan coba lagi nanti.',
    standardHeaders: true, // Return rate limit info di `RateLimit-*` headers
    legacyHeaders: false, // Disable `X-RateLimit-*` headers
    skip: (req: Request) => {
      // Skip rate limiting untuk health checks
      return req.path === '/health';
    },
  });
 
  use(req: Request, res: Response, next: NextFunction) {
    this.limiter(req, res, next);
  }
}

Throttler Guard untuk Endpoints

Buat src/common/guards/throttle.guard.ts:

src/common/guards/throttle.guard.ts
import { Injectable, CanActivate, ExecutionContext, TooManyRequestsException } from '@nestjs/common';
import { Request } from 'express';
 
interface ThrottleRecord {
  count: number;
  resetTime: number;
}
 
@Injectable()
export class ThrottleGuard implements CanActivate {
  private throttleMap = new Map<string, ThrottleRecord>();
  private readonly windowMs = 60 * 1000; // 1 menit
  private readonly maxRequests = 30; // 30 requests per menit
 
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    const key = `${request.ip}-${request.path}`;
    const now = Date.now();
 
    let record = this.throttleMap.get(key);
 
    if (!record || now > record.resetTime) {
      record = {
        count: 1,
        resetTime: now + this.windowMs,
      };
    } else {
      record.count++;
    }
 
    this.throttleMap.set(key, record);
 
    if (record.count > this.maxRequests) {
      throw new TooManyRequestsException(
        `Terlalu banyak requests. Max ${this.maxRequests} requests per menit.`,
      );
    }
 
    return true;
  }
}

Throttle Decorator

Buat src/common/decorators/throttle.decorator.ts:

src/common/decorators/throttle.decorator.ts
import { SetMetadata } from '@nestjs/common';
 
export interface ThrottleOptions {
  limit: number;
  windowMs: number;
}
 
export const Throttle = (options: ThrottleOptions) =>
  SetMetadata('throttle', options);

Advanced Throttler dengan Redis

Untuk production, gunakan Redis-backed throttling. Buat src/common/guards/redis-throttle.guard.ts:

src/common/guards/redis-throttle.guard.ts
import { Injectable, CanActivate, ExecutionContext, TooManyRequestsException } from '@nestjs/common';
import { Request } from 'express';
import * as redis from 'redis';
import { CustomLoggerService } from '../logger/logger.service';
 
@Injectable()
export class RedisThrottleGuard implements CanActivate {
  private redisClient: redis.RedisClient;
 
  constructor(private logger: CustomLoggerService) {
    this.redisClient = redis.createClient({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      password: process.env.REDIS_PASSWORD,
    });
 
    this.redisClient.on('error', (err) => {
      this.logger.error('Redis connection error', err.message, 'RedisThrottle');
    });
  }
 
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    const key = `throttle:${request.ip}:${request.path}`;
    const limit = 30;
    const windowMs = 60;
 
    return new Promise((resolve, reject) => {
      this.redisClient.incr(key, (err, count) => {
        if (err) {
          this.logger.error('Redis error', err.message, 'RedisThrottle');
          resolve(true); // Izinkan request jika Redis gagal
          return;
        }
 
        if (count === 1) {
          this.redisClient.expire(key, windowMs);
        }
 
        if (count > limit) {
          reject(
            new TooManyRequestsException(
              `Terlalu banyak requests. Max ${limit} requests per ${windowMs} detik.`,
            ),
          );
        } else {
          resolve(true);
        }
      });
    });
  }
}

Strategi Caching Redis

Mengapa Redis?

Redis menyediakan caching in-memory dengan automatic expiration, sempurna untuk mengurangi beban database dan meningkatkan response times. Kami akan mengimplementasikan caching sederhana dan strategi cache invalidation.

Buat Cache Service

Buat src/cache/cache.service.ts:

src/cache/cache.service.ts
import { Injectable } from '@nestjs/common';
import * as redis from 'redis';
import { promisify } from 'util';
import { CustomLoggerService } from '../common/logger/logger.service';
 
@Injectable()
export class CacheService {
  private redisClient: redis.RedisClient;
  private getAsync: (key: string) => Promise<string>;
  private setAsync: (key: string, value: string, mode: string, duration: number) => Promise<string>;
  private delAsync: (key: string) => Promise<number>;
  private keysAsync: (pattern: string) => Promise<string[]>;
 
  constructor(private logger: CustomLoggerService) {
    this.redisClient = redis.createClient({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      password: process.env.REDIS_PASSWORD,
    });
 
    this.getAsync = promisify(this.redisClient.get).bind(this.redisClient);
    this.setAsync = promisify(this.redisClient.set).bind(this.redisClient);
    this.delAsync = promisify(this.redisClient.del).bind(this.redisClient);
    this.keysAsync = promisify(this.redisClient.keys).bind(this.redisClient);
 
    this.redisClient.on('error', (err) => {
      this.logger.error('Redis connection error', err.message, 'Cache');
    });
 
    this.redisClient.on('connect', () => {
      this.logger.log('Redis connected', 'Cache');
    });
  }
 
  async get<T>(key: string): Promise<T | null> {
    try {
      const value = await this.getAsync(key);
      if (value) {
        this.logger.debug(`Cache hit: ${key}`, 'Cache');
        return JSON.parse(value);
      }
      this.logger.debug(`Cache miss: ${key}`, 'Cache');
      return null;
    } catch (error) {
      this.logger.error(`Cache get error untuk key ${key}`, error.message, 'Cache');
      return null;
    }
  }
 
  async set<T>(key: string, value: T, ttlSeconds: number = 3600): Promise<void> {
    try {
      await this.setAsync(key, JSON.stringify(value), 'EX', ttlSeconds);
      this.logger.debug(`Cache set: ${key} (TTL: ${ttlSeconds}s)`, 'Cache');
    } catch (error) {
      this.logger.error(`Cache set error untuk key ${key}`, error.message, 'Cache');
    }
  }
 
  async delete(key: string): Promise<void> {
    try {
      await this.delAsync(key);
      this.logger.debug(`Cache deleted: ${key}`, 'Cache');
    } catch (error) {
      this.logger.error(`Cache delete error untuk key ${key}`, error.message, 'Cache');
    }
  }
 
  async deletePattern(pattern: string): Promise<void> {
    try {
      const keys = await this.keysAsync(pattern);
      if (keys.length > 0) {
        await Promise.all(keys.map((key) => this.delete(key)));
        this.logger.debug(`Cache pattern deleted: ${pattern} (${keys.length} keys)`, 'Cache');
      }
    } catch (error) {
      this.logger.error(`Cache pattern delete error untuk ${pattern}`, error.message, 'Cache');
    }
  }
 
  async flush(): Promise<void> {
    try {
      this.redisClient.flushdb();
      this.logger.log('Cache flushed', 'Cache');
    } catch (error) {
      this.logger.error('Cache flush error', error.message, 'Cache');
    }
  }
}

Cache Decorator

Buat src/cache/decorators/cache.decorator.ts:

src/cache/decorators/cache.decorator.ts
import { SetMetadata } from '@nestjs/common';
 
export interface CacheOptions {
  key: string;
  ttl?: number; // Time to live dalam detik
}
 
export const CacheKey = (options: CacheOptions) => SetMetadata('cache', options);

Cache Interceptor

Buat src/cache/interceptors/cache.interceptor.ts:

src/cache/interceptors/cache.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Request } from 'express';
import { Reflector } from '@nestjs/core';
import { CacheService } from '../cache.service';
import { CacheOptions } from '../decorators/cache.decorator';
 
@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(
    private reflector: Reflector,
    private cacheService: CacheService,
  ) {}
 
  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const cacheOptions = this.reflector.get<CacheOptions>('cache', context.getHandler());
 
    if (!cacheOptions) {
      return next.handle();
    }
 
    const request = context.switchToHttp().getRequest<Request>();
    const cacheKey = `${cacheOptions.key}:${request.user?.id || 'anonymous'}`;
 
    // Coba ambil dari cache
    const cachedData = await this.cacheService.get(cacheKey);
    if (cachedData) {
      return of(cachedData);
    }
 
    // Jika tidak di cache, jalankan handler dan cache hasilnya
    return next.handle().pipe(
      tap(async (data) => {
        const ttl = cacheOptions.ttl || 3600;
        await this.cacheService.set(cacheKey, data, ttl);
      }),
    );
  }
}

Cache Module

Buat src/cache/cache.module.ts:

src/cache/cache.module.ts
import { Module } from '@nestjs/common';
import { CacheService } from './cache.service';
import { LoggerModule } from '../common/logger/logger.module';
 
@Module({
  imports: [LoggerModule],
  providers: [CacheService],
  exports: [CacheService],
})
export class CacheModule {}

Menyatukan Semuanya - Contoh Lengkap

Prisma Service

Buat src/prisma/prisma.service.ts:

src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
 
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }
 
  async onModuleDestroy() {
    await this.$disconnect();
  }
}

Buat src/prisma/prisma.module.ts:

src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import

Related Posts