- Ringkasan
- Fitur Utama
- Arsitektur & Cara Kerja
- Dependency Injection
- Struktur Direktori
- Modul Bawaan
- Konfigurasi Lingkungan
- Siklus Bootstrap
- Middleware & Rate Limiter
- Socket Opsional
- Fungsi Penting
- Penggunaan Harian
- Automasi Modul
- Deployment
- FAQ
- Kontribusi
- Lisensi
Templat ini membantu Anda membangun REST API Node.js berbasis TypeScript dengan arsitektur modular, dependency injection, dan pilihan framework HTTP (Express atau Fastify). Seluruh komponen dirancang agar plug-and-play; Anda cukup memilih middleware, mendefinisikan modul, lalu menjalankan bootstrap.
- Modular Monolith: Setiap fitur ditempatkan dalam modul independen, siap dipecah menjadi microservice.
- Multi HTTP Provider: Ganti
HTTP_SERVER=express|fastifyuntuk beralih framework tanpa menyentuh kode modul. - Dependency Injection via Tsyringe: Kontruksi service yang eksplisit dan mudah diuji.
- Bootstrap Terpusat: File
main.tshanya mengatur middleware & modul, sedangkan kelasAppmenangani lifecycle. - Middleware Plug-and-Play: Daftarkan middleware per provider dengan satu fungsi utilitas.
- Rate Limiting: Aktivasi otomatis via middleware global atau per modul.
- Automasi Modul: CLI
pnpm create:modulemembuat struktur modul lengkap dengan test. - Infra-Ready: Konfigurasi jelas, logger Pino, script build deploy Docker.
- Integrasi Database Fleksibel: Template menyertakan contoh repository Prisma sekaligus mendukung implementasi lain (in-memory, REST, dsb) tanpa mengubah service.
-
Entry Point (
src/main.ts)- Memuat variabel lingkungan (
env). - Membuat server HTTP sesuai provider.
- Membuat instance
Appdan mendaftarkan middleware + modul (termasuk route health). - Menjalankan
app.start().
- Memuat variabel lingkungan (
-
AppLifecycle (src/core/App.ts)- Menyimpan middleware, modul, dan cleanup callback.
- Saat start:
- Mendaftarkan middleware ke server.
- Mendaftarkan modul ke server.
- Mengikat sinyal shutdown.
- Memerintahkan server listen pada port.
-
HTTP Abstraction (
src/core/http)ExpressHttpServerdanFastifyHttpServermengimplementasikan kontrakHttpServer.- Wrapper menangani konversi
RouteHandlerinternal ke handler framework.
-
Modul
- Memperluas route dengan prefix
api/v1/<modul>. - Bisa mengembalikan opsi (contoh rate limit per modul).
- Memperluas route dengan prefix
- Menggunakan
tsyringeuntuk mendaftarkan service, controller, repository, dan provider infrastruktur seperti Prisma. - Mengandalkan decorator
@injectable()/@singleton()dan constructor injection sehingga dependency tidak pernah diakses secara global. - Kontrak dipresentasikan sebagai interface + token (contoh:
IUsersRepository+USERS_REPOSITORY_TOKEN) agar implementasi mudah diganti tanpa menyentuh service. - Registrasi binding dilakukan di berkas container per modul (contoh:
users.container.ts) supaya struktur modul tetap terisolasi.
UsersController @injectable()
└── constructor(@inject(UsersService))
UsersService @injectable()
└── constructor(@inject(USERS_REPOSITORY_TOKEN) IUsersRepository)
users.container.ts
└── container.registerSingleton(USERS_REPOSITORY_TOKEN, PrismaUsersRepository)
PrismaUsersRepository @injectable()
└── constructor(@inject(PrismaService)) -> prisma.user.findMany()
PrismaService @singleton()
└── PrismaClient (dikonfigurasi via env)
- Registrasi otomatis via decorator sudah cukup; hindari container manual kecuali untuk pengujian.
- Gunakan interface untuk kontrak repository bila akan mengganti implementasi.
- Dalam test, gunakan
container.registerdenganuseValueatauuseClassuntuk mengganti dependensi. - Pastikan memanggil
container.clearInstances()bila membuat container custom di test. - Jika ingin menambahkan integrasi eksternal (misal S3 atau Redis), buat abstraksi baru (mis.
StorageService), tandai dengan@injectable(), lalu injeksikan ke service lain. Di test, Anda bisa menggantiStorageServicedengan mock menggunakancontainer.register(StorageService, { useValue: mockObj }). - Hindari mengakses container secara global di dalam fungsi. Lebih baik injeksikan dependensi melalui konstruktor agar komponen tetap testable dan tidak bergantung pada state global.
- Setiap modul bisa menyediakan beberapa implementasi repository (misal in-memory, REST client, Prisma). Penamaan
users.prisma.repository.tsmenegaskan bahwa implementasi tersebut mengandalkan Prisma; sementara nama generikusers.repository.tscocok untuk implementasi default. users.container.tsbertugas mengikat kontrakUSERS_REPOSITORY_TOKENke implementasi yang dipilih. Anda cukup mengganti binding ini bila ingin repository lain tanpa mengubah service ataupun controller.- Infrastruktur bersama (contoh
PrismaService) ditaruh disrc/shared/infra/...dan disediakan sebagai dependency tersendiri sehingga modul lain dapat menggunakannya dengan tetap memenuhi prinsip Single Responsibility & Dependency Inversion. - Untuk menambahkan repository alternatif, buat file baru (contoh
users.in-memory.repository.ts) yang mengimplementasikanIUsersRepository, lalu perbarui binding pada container modul.
Contoh integrasi S3 menggunakan DI:
// storage/StorageService.ts
import { injectable } from 'tsyringe';
@injectable()
export class StorageService {
async upload(fileName: string, buffer: Buffer): Promise<string> {
// Integrasi S3 (contoh sederhana)
return `https://s3.example.com/${fileName}`;
}
}
// modules/reports/reports.service.ts
import { inject, injectable } from 'tsyringe';
import { StorageService } from '@/storage/StorageService';
@injectable()
export class ReportsService {
constructor(@inject(StorageService) private readonly storage: StorageService) {}
async generateReport(payload: Buffer): Promise<string> {
const fileName = `report-${Date.now()}.pdf`;
return this.storage.upload(fileName, payload);
}
}
// modules/reports/reports.service.spec.ts
import 'reflect-metadata';
import { container } from 'tsyringe';
import { ReportsService } from './reports.service';
import { StorageService } from '@/storage/StorageService';
describe('ReportsService', () => {
it('mengunggah report ke storage', async () => {
const uploadMock = jest.fn().mockResolvedValue('https://mock/report.pdf');
container.register(StorageService, { useValue: { upload: uploadMock } });
const service = container.resolve(ReportsService);
const url = await service.generateReport(Buffer.from('dummy'));
expect(uploadMock).toHaveBeenCalledTimes(1);
expect(url).toBe('https://mock/report.pdf');
});
});src/
config/ # Konfigurasi environment
core/
App.ts # Lifecycle aplikasi
http/
createHttpServer.ts
createMiddlewares.ts
ExpressHttpServer.ts
FastifyHttpServer.ts
middleware/ # Handler umum (error, dll)
modules/
loadModules.ts # Loader modul berdasarkan konfigurasi
users/ # Contoh modul users
whatsapp/ # Contoh modul whatsapp
shared/
utils/logger.ts # Logger Pino
main.ts # Entry point
Agar konsisten, setiap modul sebaiknya memuat berkas berikut:
*.routes.ts– Mendefinisikan prefix modul serta daftar rute yang diekspos.*.controller.ts– Mengelola request/response dan memanggil service yang relevan.*.service.ts– Menampung logika bisnis utama modul.*.repository.ts/*.prisma.repository.ts– Layer akses data; bebas memilih penamaan sesuai backend yang digunakan.*.interface.ts– Deklarasi tipe data yang digunakan modul.*.service.spec.ts– Unit test service dengan contoh mocking dependency injection.*.container.ts– Titik registrasi dependency modul (binding interface ke implementasi).
Gunakan
pnpm create:moduleuntuk menghasilkan kerangka modul baru, lalu daftar namanya padaavailableModulesdandevModeModulesjika ingin langsung aktif saat development.
Variabel utama didefinisikan di src/config/index.ts dan tervalidasi menggunakan Zod.
| Variabel | Default | Deskripsi |
|---|---|---|
NODE_ENV |
development | Lingkungan aplikasi |
PORT |
3000 | Port aplikasi |
HTTP_SERVER |
express | Provider HTTP (express/fastify) |
DATABASE_URL |
file:./dev.db | Connection string Prisma (SQLite default) |
SOCKET_ENABLED |
false | Aktifkan integrasi Socket.IO ketika diset true |
Contoh .env:
NODE_ENV=development
PORT=3000
HTTP_SERVER=fastify
DATABASE_URL=file:./dev.db
SOCKET_ENABLED=falseFile contoh tersedia di .env.example; salin ke .env lalu sesuaikan nilainya.
main.tsmemanggilcreateHttpServerdengan logger.- Buat instance
App(menerima server, port, logger). createGlobalMiddlewaresmengembalikan daftar middleware sesuai provider.loadConfiguredModulesmemuat modul daridevModeModules.app.start()mengaktifkan middleware, modul, sinyal shutdown, lalu listen.
- Express: Menggunakan
cors()danrateLimit()default global. - Fastify: Registrasi plugin
@fastify/corsdan@fastify/rate-limit. - Middleware tambahan dapat diregister di
main.tsmelaluiapp.registerMiddleware({...}). - Rate limit per modul dapat ditambahkan via
module.options.rateLimitpada file route modul. - Menonaktifkan CORS: Hapus middleware CORS pada array
middlewaresdimain.ts. - Menonaktifkan Rate Limit: Hapus middleware limiter dari
main.tsdan hilangkan konfigurasioptions.rateLimitpada modul.
Contoh rate limit per modul (users.routes.ts):
export default function createUsersRoutes(): ModuleBuildResult {
return {
routes,
options: { rateLimit: { windowMs: 60_000, max: 100 } },
};
}- Aktifkan dengan menyetel
SOCKET_ENABLED=true. Saat aktif,main.tsmendaftarkan adapter Socket.IO yang beroperasi di atas server HTTP yang sama. - Gunakan di modul melalui DI: token
SOCKET_IO_SERVER_TOKENmengekspos instanceSocketIoServer. Injeksi via constructor:import { inject, injectable } from 'tsyringe'; import type { SocketIoServer } from '@/core/socket/socketIoAdapter'; import { SOCKET_IO_SERVER_TOKEN } from '@/core/socket/socketIoAdapter'; @injectable() export class NotificationsService { constructor( @inject(SOCKET_IO_SERVER_TOKEN) private readonly io: SocketIoServer, ) {} broadcast(message: string) { this.io.emit('notifications:new', { message }); } }
- Nonaktifkan dengan membiarkan
SOCKET_ENABLED=false(default) atau menghapus variabel tersebut. Adapter tidak akan didaftarkan sehingga modul tetap berjalan tanpa dependensi socket.
| Lokasi | Fungsi |
|---|---|
src/main.ts |
Entry point aplikasi |
App.registerMiddleware |
Menambahkan middleware global |
App.registerModule |
Menambahkan modul (prefix + routes) |
App.start |
Menjalankan server dan mengikat shutdown hook |
createHttpServer(provider, logger) |
Membuat instance server Express/Fastify |
createGlobalMiddlewares(provider) |
Menghasilkan array middleware untuk provider tertentu |
loadConfiguredModules(logger) |
Memuat modul yang terdaftar di deployment.config.ts |
PrismaUsersRepository.findAll |
Contoh repository berbasis Prisma |
users.container.ts |
Mengikat kontrak USERS_REPOSITORY_TOKEN ke implementasi |
Logger.info/error |
Helper logging dengan Pino |
pnpm dev- Jalankan server dengan watch mode (menggunakan
tsx --watch). - Endpoint health:
GET /health.
pnpm test- Menjalankan Jest unit test (contoh: service users & whatsapp).
pnpm build- Menghasilkan bundel di
dist/menggunakantsup. - Jalankan hasil build:
pnpm start.
pnpm create:module- Wizard interaktif akan membuat controller, service, repository, route, dan test.
- Setelah selesai, tambahkan modul ke
availableModulesdandevModeModules(jika perlu).
- Build artefak deploy:
pnpm build:deploys - Masuk ke salah satu target:
cd deploys/main-api - Build image:
docker build -t main-api . - Jalankan container:
docker run --env-file .env.production -p 3000:3000 main-api
Tips:
- Pastikan
.env.productionberisi konfigurasi yang benar. - Gunakan registry privat bila dibutuhkan, contoh
docker push ghcr.io/<org>/main-api:tag.
- Build aplikasi:
pnpm build - Jalankan menggunakan PM2:
pm2 start dist/main.js --name api-template pm2 logs api-template pm2 save
- Untuk restart otomatis saat update:
pm2 restart api-template
Bisakah menambahkan basis data?
Ya. Contoh modul users sudah menggunakan Prisma melalui PrismaUsersRepository. Jika ingin backend lain (REST, in-memory untuk test, ORM berbeda), buat implementasi baru untuk IUsersRepository dan ubah binding di users.container.ts.
Bagaimana menambahkan middleware custom?
Di main.ts, panggil app.registerMiddleware({ express: middlewareExpress }) atau app.registerMiddleware({ fastify: middlewareFastify }) sesuai provider.
Bisakah menjalankan Express & Fastify bersamaan?
Tidak secara simultan. Pilih satu provider melalui variabel HTTP_SERVER.
- Fork repo, buat branch fitur, lalu pull request.
- Sertakan deskripsi perubahan dan tambahkan test bila relevan.
- Gunakan format commit konvensional bila memungkinkan.
Dirilis di bawah lisensi MIT.