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.
Mulai dengan membuat proyek NestJS baru:
npm i -g @nestjs/cli
nest new nestjs-api
cd nestjs-api
Sekarang instal semua dependensi yang diperlukan:
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:
# 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:
cp .env.example .env.local
Inisialisasi Prisma di proyek Anda:
Perbarui prisma/schema.prisma dengan model database Anda:
// 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
Buat 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:
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 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 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 {}
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 ();
}
}
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 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' );
}
}
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 ,
};
}
}
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 ;
}
}
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 );
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' };
}
}
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' ) {}
Buat 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 : Membatasi requests per IP address secara global
Throttling : Membatasi requests per user/endpoint, kontrol yang lebih granular
Kami akan mengimplementasikan keduanya untuk perlindungan komprehensif.
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 );
}
}
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 ;
}
}
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 );
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 );
}
});
});
}
}
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 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' );
}
}
}
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 );
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 );
}),
);
}
}
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 {}
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