Panduan Pemrograman Nest JS Typescript
A). Development Environment Setup
System Requirements
Untuk memastikan lingkungan pengembangan yang konsisten, ikuti langkap-langkah berikut ini:
- Operating System: Windows 10/11 atau macOS (Apple Silicon/Intel)
- Node.js: Versi 20.* (Gunakan versi LTS)
- Database Server: PostgreSQL atau MySQL (Docker atau local installation)
- Cache: Redis (Docker atau local installation)
- Package Manager:
yarn
/pnpm
/npm
- Source Control: DOT GitLab Self-hosted (https://gitlab.dot.co.id)
Tools & IDE
Berikut daftar tools yang bisa digunakan oleh setiap developer:
- IDE:
- Visual Studio Code (Disarankan)
- JetBrains WebStorm (Alternatif)
- Database Management GUI:
- DBeaver (Disarankan)
- Redis Management:
- RedisInsight
- Git Client:
- CLI atau GUI seperti Fork, GitKraken, atau SourceTree
- API Testing:
- Apidog (Disarankan)
- Postman
Local Development Preparation
Instalasi Node.js (Menggunakan NVM)
Petunjuk instalasi bisa diakses disini
nvm install --lts nvm use --lts
Setup Database menggunakan Docker
PostgreSQL
docker run --name Postgres -e POSTGRES_PASSWORD=YourPassword! -d -p 0.0.0.0:5432:5432 -v postgres_data:/var/lib/postgresql/data postgres
MySQL
docker run --name='MySQL' -d -p 0.0.0.0:3306:3306 mysql/mysql-server:8.0.28
Setup Redis menggunakan Docker
docker run --name redis -p 6379:6379 -d redis
Development Best Practices
- Gunakan Docker untuk menjalankan service dependency agar konsisten.
- Pastikan semua environment variable didefinisikan dalam appsettings.Development.json.
- Pastikan semua service berjalan sebelum memulai development (docker ps).
B). Project Structure & Anatomy
Struktur base proyek dirancang untuk memisahkan concern setiap komponen agar mudah dikelola, dikembangkan, dan di-scale. Mengikuti prinsip Clean Architecture, proyek dibagi menjadi beberapa layer dengan tanggung jawab masing-masing.
├── api
│ ├── assets
│ ├── dist
│ ├── src
│ │ ├── cache
│ │ ├── common
│ │ │ ├── constants
│ │ │ ├── enums
│ │ │ ├── filters
│ │ │ ├── interceptors
│ │ │ ├── interface
│ │ │ ├── pipes
│ │ │ ├── request
│ │ │ ├── rules
│ │ │ └── utils
│ │ ├── health
│ │ ├── infrastructure
│ │ │ ├── applications
│ │ │ ├── cache
│ │ │ │ ├── decorators
│ │ │ │ ├── interceptors
│ │ │ │ ├── middlewares
│ │ │ │ └── services
│ │ │ ├── error
│ │ │ ├── mail
│ │ │ │ └── templates
│ │ │ ├── notification
│ │ │ │ └── services
│ │ │ └── schedules
│ │ └── modules
│ │ ├── auth
│ │ │ ├── applications
│ │ │ ├── controllers
│ │ │ │ └── v1
│ │ │ ├── dto
│ │ │ ├── guards
│ │ │ ├── responses
│ │ │ ├── serializers
│ │ │ ├── services
│ │ │ └── strategies
│ │ ├── common
│ │ │ ├── applications
│ │ │ ├── controllers
│ │ │ │ └── v1
│ │ │ ├── dto
│ │ │ └── services
│ │ ├── queue
│ │ │ ├── contants
│ │ │ ├── contracts
│ │ │ └── services
│ │ └── user
│ │ ├── applications
│ │ ├── controllers
│ │ │ └── v1
│ │ ├── request
│ │ ├── responses
│ │ └── services
│ └── storages
├── backoffice
│ ├── @contracts
│ │ └── auth
│ │ ├── request
│ │ └── schema
│ ├── app
│ │ ├── Components
│ │ │ ├── atoms
│ │ │ │ └── Button
│ │ │ ├── molecules
│ │ │ │ ├── Breadcrumbs
│ │ │ │ ├── DescriptionContainer
│ │ │ │ ├── Dropdowns
│ │ │ │ ├── Form
│ │ │ │ ├── Headers
│ │ │ │ ├── Pickers
│ │ │ │ ├── Progress
│ │ │ │ ├── RowActionButtons
│ │ │ │ ├── Section
│ │ │ │ └── TimelinesItem
│ │ │ └── organisms
│ │ │ ├── DataTable
│ │ │ ├── FilterSection
│ │ │ │ └── InputCollection
│ │ │ └── FormContainer
│ │ ├── Contexts
│ │ ├── Enums
│ │ ├── Layouts
│ │ │ ├── Login
│ │ │ └── MainLayout
│ │ ├── Modules
│ │ │ ├── Auth
│ │ │ │ ├── ForgotPassword
│ │ │ │ └── Login
│ │ │ ├── Common
│ │ │ ├── Inertia
│ │ │ ├── Notification
│ │ │ ├── Page
│ │ │ ├── Profile
│ │ │ └── User
│ │ ├── Pages
│ │ │ ├── Configs
│ │ │ ├── Iam
│ │ │ │ ├── Permissions
│ │ │ │ ├── RolePermissions
│ │ │ │ ├── Roles
│ │ │ │ └── Users
│ │ │ ├── LogActivities
│ │ │ ├── Notifications
│ │ │ ├── Profile
│ │ │ └── Sample
│ │ │ ├── Detail
│ │ │ └── Form
│ │ ├── Types
│ │ └── Utils
│ ├── assets
│ ├── public
│ │ ├── css
│ │ ├── img
│ │ ├── js
│ │ ├── lib
│ │ │ ├── bootstrap
│ │ │ │ └── dist
│ │ │ │ ├── css
│ │ │ │ └── js
│ │ │ ├── jquery
│ │ │ │ └── dist
│ │ │ ├── jquery-validation
│ │ │ │ └── dist
│ │ │ └── jquery-validation-unobtrusive
│ │ ├── temp
│ │ └── unity
│ │ ├── css
│ │ ├── img
│ │ └── js
│ │ └── lib
│ ├── src
│ │ ├── common
│ │ │ ├── enums
│ │ │ ├── filters
│ │ │ ├── interceptors
│ │ │ ├── interface
│ │ │ ├── pipes
│ │ │ ├── request
│ │ │ ├── rules
│ │ │ └── utils
│ │ ├── infrastructure
│ │ │ ├── ability
│ │ │ ├── applications
│ │ │ ├── cache
│ │ │ │ ├── decorators
│ │ │ │ ├── middlewares
│ │ │ │ └── services
│ │ │ ├── entities
│ │ │ │ └── subscribers
│ │ │ ├── error
│ │ │ ├── event
│ │ │ ├── inertia
│ │ │ │ ├── adapter
│ │ │ │ ├── entities
│ │ │ │ └── middlewares
│ │ │ ├── mail
│ │ │ │ └── templates
│ │ │ ├── notification
│ │ │ │ └── services
│ │ │ ├── redis
│ │ │ ├── sentry
│ │ │ └── serializers
│ │ └── modules
│ │ ├── auth
│ │ │ ├── applications
│ │ │ ├── controllers
│ │ │ ├── guards
│ │ │ ├── requests
│ │ │ ├── responses
│ │ │ ├── serializers
│ │ │ ├── services
│ │ │ └── strategies
│ │ ├── common
│ │ │ └── controllers
│ │ ├── config
│ │ │ ├── applications
│ │ │ ├── controllers
│ │ │ ├── guards
│ │ │ ├── mappers
│ │ │ ├── requests
│ │ │ ├── responses
│ │ │ └── services
│ │ ├── glob
│ │ │ └── service
│ │ ├── iam
│ │ │ ├── applications
│ │ │ ├── controllers
│ │ │ ├── decorators
│ │ │ ├── guards
│ │ │ ├── mappers
│ │ │ ├── middlewares
│ │ │ ├── policies
│ │ │ ├── requests
│ │ │ ├── responses
│ │ │ └── services
│ │ ├── log-activity
│ │ │ ├── applications
│ │ │ ├── controllers
│ │ │ ├── requests
│ │ │ ├── responses
│ │ │ └── services
│ │ ├── main
│ │ │ └── controllers
│ │ ├── notification
│ │ │ ├── applications
│ │ │ ├── controllers
│ │ │ ├── middlewares
│ │ │ ├── requests
│ │ │ └── services
│ │ ├── profile
│ │ │ ├── applications
│ │ │ ├── controllers
│ │ │ ├── requests
│ │ │ ├── responses
│ │ │ └── services
│ │ └── queue
│ │ ├── contants
│ │ ├── contracts
│ │ └── services
│ └── storages
└── graphql
├── assets
├── public
└── src
├── common
│ ├── enums
│ ├── filters
│ ├── guards
│ ├── interface
│ ├── middlewares
│ ├── request
│ └── utils
├── infrastructure
│ ├── applications
│ └── cache
│ ├── decorators
│ ├── middlewares
│ └── services
└── modules
├── auth
│ ├── applications
│ ├── resolvers
│ ├── services
│ └── types
└── iam
├── applications
├── mutations
├── resolvers
├── services
└── types
C). Code Style and Principle
Naming Convention
File Name
- Use
kebab-case
for file name - Include the file type in the name
user.controller.ts
user.service.ts
user.entity.ts
user.interface.ts
export class UserController {}
export class UserService {}
export class CreateUserDto {}
export interface IUser
async findUserById(userId: string): Promise<User> {}
const userPassword: string;
Code Concistency
Using Enum
Gunakan Enum untuk mendefinisikan nilai tetap yang memiliki keterkaitan, seperti status pengguna atau role.
❌ Bad: (Menggunakan string hardcode untuk role secara langsung)
export class User {
id: number;
name: string;
role: string; // Tidak disarankan
}
// Contoh penggunaan
const user: User = { id: 1, name: 'John', role: 'admin' }; // Rentan typo
console.log(user.role); // Output: 'admin'
✅ Good: (Menggunakan enum
)
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator',
}
export class User {
id: number;
name: string;
role: UserRole;
}
// Contoh penggunaan
const user: User = { id: 1, name: 'John', role: UserRole.ADMIN };
console.log(user.role); // Output: 'admin'
Using Constant
- Gunakan constant (
const
) untuk nilai tetap yang tidak berubah. ❌ Bad: (Menggunakan magic number atau string langsung)
export class AuthService {
loginAttempts: number = 0;
login() {
if (this.loginAttempts >= 5) { // Magic number, sulit diubah
throw new Error('Too many login attempts');
}
this.loginAttempts++;
}
}
// application.constant.ts
export const MAX_LOGIN_ATTEMPTS = 5;
// auth.service.ts
export class AuthService {
loginAttempts: number = 0;
login() {
if (this.loginAttempts >= MAX_LOGIN_ATTEMPTS) {
throw new Error('Too many login attempts');
}
this.loginAttempts++;
}
}
export class UserService {
getUserById(id: number): any {
return { id, name: 'John Doe', role: 'user' };
}
}
export class UserService {
getUserById(id: number): Promise<User | null> {
return new Promise((resolve) => {
resolve({ id, name: 'John Doe', role: UserRole.USER });
});
}
}
Clean / Layering Pattern Principle
- Presentation Layer (Controller)
- Presentation Layer atau Controller memiliki responsibility untuk melakukan handle request dan response atas client.
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
async createUser(@Body() dto: CreateUserDto) {
return this.userService.createUser(dto);
}
}
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async createUser(dto: CreateUserDto) {
const user = await this.userRepository.create(dto);
return user;
}
}
@Injectable()
export class UserRepository {
constructor(
@InjectRepository(UserEntity)
private readonly repository: Repository<UserEntity>,
) {}
async create(dto: CreateUserDto): Promise<UserEntity> {
return this.repository.save(dto);
}
}
D). Database & Query Effeciency
- Menggunakan TypeORM sebagai database ORM [Link]
- Pastikan setiap query optimal dengan memanfaatkan indexing pada kolom yang sering dicari
❌ Bad: (Tidak menggunakan indexing pada column)
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
}
✅ Good: (Menggunakan indexing pada column)
@Entity()
@Index(['email', 'name']) // Menambahkan index pada column
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
}
- Gunakan pagination dengan
take
danskip
ketika melakukan get all data, agar tidak membebani database
❌ Bad: (Tidak menggunakan pagination untuk data yang besar)
const notifications = await this.notificationRepository.find();
✅ Good: (Menggunakan pagination untuk data yang besar)
const notifications = await this.notificationRepository.find({
take: 10,
skip: 20
});
- Gunakan DB Transaction jika ada proses yang melakukan operasi CREATE / UPDATE / DELETE pada multi-table [Link]
❌ Bad: (Melakukan query satu per satu tanpa transaction, rawan terjadi inkonsistensi data jika salah satu terjadi error)
const order = this.orderRepository.create({ userId, totalPrice: 100 });
await this.orderRepository.save(order);
const payment = this.paymentRepository.create({ orderId: order.id, status: 'pending' });
await this.paymentRepository.save(payment);
✅ Good: (Menggunakan transaction pada operasi multi-table)
await this.dataSource.transaction(async (manager) => {
const order = manager.create(Order, { userId, totalPrice: 100 });
await manager.save(order);
const payment = manager.create(Payment, { orderId: order.id, status: 'pending' });
await manager.save(payment);
});
- Hindari melakukan select semua field / column, selalu tentukan field yang akan diperlukan
❌ Bad: (Mengambil semua kolom, membebani database)
const user = await this.userRepository.findOne({
where: { id },
});
✅ Good: (Mengambil hanya kolom yang dibutuhkan)
const user = await this.userRepository
.createQueryBuilder('user')
.select(['user.id', 'user.name', 'user.email'])
.where('user.id = :id', { id })
.getOne();
E). Error Handling & Logging
- Implementasikan Global Exception Filter untuk menangani error secara global [Link]
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// Custom your exception handling logic here
}
}
- Implementasikan Custom Exception jika ada kebutuhan exception yang spesifik [Link]
// otp-invalid.exception.ts
export class OtpInvalidException extends HttpException {
constructor(
super('Invalid OTP', HttpStatus.BAD_REQUEST)
) {}
}
// auth-otp.service.ts
export class AuthOtpService {
constructor() {}
async validateOtp(otpRequest: OtpRequestDto) {
const isValid = true;
if (!isValid) {
throw new OtpInvalidException();
}
}
}
- Implementasikan Logging untuk proses yang memerlukan monitoring state (ex: sync data, background proses, etc) [Link]
import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { AppLogger } from '../logger/logger.service';
@Injectable()
export class SyncService {
private readonly logger = new Logger(SyncService.name);
@Cron('0 * * * *') // Jalankan setiap jam
async syncData(): Promise<void> {
this.logger.log('Memulai sinkronisasi data dari background process...');
try {
// Simulasi proses sinkronisasi
await new Promise((resolve) => setTimeout(resolve, 3000));
this.logger.log('Data berhasil disinkronisasi dari background process.');
} catch (error) {
this.logger.error('Error saat sinkronisasi data', error.stack);
}
}
}
F). Security
- Gunakan
min
danmax
(opsional) untuk data payload dengan typenumber
pada DTO
❌ Bad:
const TopupSaldoSchema = z.object({
amount: z.number(), // Tidak menggunakan min value
})
export class TopupSaldoDto extends createZodDto(TopupSaldoSchema) {}
✅ Good:
import { z } from 'zod';
import { createZodDto } from 'nestjs-zod'
const TopupSaldoSchema = z.object({
amount: z.number().min(10_000), // Menggunakan min value untuk menghindari nilai negatif
})
export class TopupSaldoDto extends createZodDto(TopupSaldoSchema) {}
- Gunakan
@IsUUID()
untuk data payload dengan typestring
yang ekspektasinya akan menerima nilai UUID pada DTO
❌ Bad:
import { z } from 'zod';
import { createZodDto } from 'nestjs-zod'
const GetUserSchema = z.object({
userId: z.string(), // Tidak menggunakan type uuid
})
export class GetUserDto extends createZodDto(GetUserSchema) {}
✅ Good:
import { z } from 'zod';
import { createZodDto } from 'nestjs-zod'
const GetUserSchema = z.object({
userId: z.string().uuid("Format tidak valid"), // Tidak menggunakan type uuid
})
export class GetUserDto extends createZodDto(GetUserSchema) {}
- Gunakan nilai
@MinLength()
dan@MaxLength()
untuk melakukan validasi atas data payload dengan typestring
. Gunakan nilai@MaxLength()
sesuai nilai byte database untuk columntext
❌ Bad:
import { z } from 'zod';
import { createZodDto } from 'nestjs-zod'
export const RegisterUserSchema = z.object({
password: z.string()
.min(8, "Password minimal 8 karakter")
.max(32, "Password maksimal 32 karakter"),
description: z.string()
.max(255, "Deskripsi maksimal 255 karakter"),
});
export class RegisterUserDto extends createZodDto(RegisterUserSchema) {}
✅ Good:
import { z } from 'zod';
import { createZodDto } from 'nestjs-zod'
export const RegisterUserSchema = z.object({
password: z.string()
.min(8, "Password minimal 8 karakter")
.max(32, "Password maksimal 32 karakter"),
description: z.string()
.max(255, "Deskripsi maksimal 255 karakter"),
});
export class RegisterUserDto extends createZodDto(RegisterUserSchema) {}
- Tidak meletakkan endpoint atau informasi credential yang bersifat private secara hard code di dalam source code.
❌ Bad:
export class SendDataToExternal {
private secretKey = 'ThisIsN0tSuppOs3dToBeHere';
private prodUrl = 'https://someprivateip/api';
async sendData() {
// logic
}
}
✅ Good:
// .env
SECRET_KEY="ThisIsSuppOs3dToBeHere"
PROD_URL="https://someprivateip/api"
// config.ts
import * as dotenv from 'dotenv';
dotenv.config();
export const config = {
secretKey: process.env.SECRET_KEY || '',
prodUrl: process.env.PROD_URL || ''
};
// send-data-to-external.service.ts
export class SendDataToExternal {
private secretKey = 'ThisIsN0tSuppOs3dToBeHere';
private prodUrl = 'https://someprivateip/api';
async sendData() {
// logic
}
}
G). Configuration
- Semua konfigurasi yang sifatnya rahasia harus diset melalui file
config.ts
yang nilai nya diambil dari file.env
❌ Bad:
// config.ts (BAD)
export const config = {
dbHost: 'localhost',
dbUser: 'admin',
dbPassword: 'secret123', // Informasi sensitif langsung di-hardcode
dbName: 'mydb',
};
✅ Good:
// .env
DB_HOST=localhost
DB_USER=admin
DB_PASSWORD=secret123
DB_NAME=mydb
// config.ts
import * as dotenv from 'dotenv';
dotenv.config();
export const config = {
dbHost: process.env.DB_HOST || '',
dbUser: process.env.DB_USER || '',
dbPassword: process.env.DB_PASSWORD || '',
dbName: process.env.DB_NAME || '',
};
- Grouping
.env
sesuai dengan kegunaannya dan gunakan prefix yang sama
❌ Bad:
KEY=abc123
SECRET=secret456
HOST=localhost
USER=admin
PASSWORD=secret123
NAME=mydb
✅ Good:
# Database Configuration
DB_HOST=localhost
DB_USER=admin
DB_PASSWORD=secret123
DB_NAME=mydb
# API Configuration
API_KEY=abc123
API_SECRET=secret456