Memahami Monorepo dan Nx
Pengantar: Mengapa Arsitektur Kode Penting?
Bayangkan Anda memiliki sebuah perusahaan teknologi yang mengembangkan berbagai aplikasi: website e-commerce, aplikasi mobile, API backend, dan sistem admin. Setiap tim mengerjakan proyeknya di repository Git yang terpisah. Suatu hari, Anda perlu mengubah fungsi autentikasi yang digunakan di semua aplikasi. Anda harus membuka empat repository berbeda, melakukan perubahan yang sama empat kali, dan memastikan semuanya tetap sinkron. Merepotkan, bukan?
Di sinilah konsep monorepo hadir sebagai solusi. Mari kita eksplorasi konsep ini secara mendalam, mulai dari dasar hingga implementasi praktis menggunakan Golang.
Apa Itu Monorepo?
Monorepo adalah singkatan dari "monolithic repository", yaitu strategi manajemen kode di mana Anda menyimpan banyak proyek atau aplikasi dalam satu repository Git. Berbeda dengan pendekatan multi-repo atau polyrepo di mana setiap proyek memiliki repository sendiri, monorepo menempatkan semuanya dalam satu tempat yang terpusat.
Untuk memahami perbedaannya, mari kita bandingkan kedua pendekatan ini. Dalam arsitektur multi-repo tradisional, jika Anda memiliki tiga aplikasi (misalnya: aplikasi web, API, dan worker service), Anda akan memiliki tiga repository terpisah. Setiap perubahan pada kode yang dibagikan antar aplikasi memerlukan proses versioning, publikasi package, dan update dependencies di setiap repository. Proses ini bisa memakan waktu dan rentan kesalahan.
Sebaliknya, dalam monorepo, ketiga aplikasi tersebut berada dalam satu repository dengan struktur folder yang terorganisir. Ketika Anda mengubah kode yang dibagikan, semua aplikasi yang menggunakannya langsung terpengaruh dalam commit yang sama. Ini memberikan beberapa keuntungan signifikan seperti refactoring yang lebih mudah, code sharing yang lebih natural, dan atomic changes di mana perubahan pada shared code dan konsumennya bisa dilakukan dalam satu commit.
Perusahaan besar seperti Google, Facebook, dan Microsoft menggunakan monorepo untuk mengelola jutaan baris kode mereka. Google, misalnya, menyimpan hampir seluruh kode perusahaan dalam satu monorepo raksasa yang diakses oleh ribuan engineer setiap harinya.
Memahami Nx: Build System untuk Monorepo
Setelah memahami konsep monorepo, pertanyaan berikutnya adalah: bagaimana mengelola monorepo yang kompleks secara efisien? Di sinilah Nx berperan.
Nx adalah build system dan toolkit yang powerful untuk mengelola monorepo. Meskipun awalnya dikembangkan untuk ekosistem JavaScript/TypeScript, konsep dan arsitekturnya bisa diterapkan pada bahasa lain termasuk Golang. Nx menyediakan berbagai fitur cerdas yang membuat bekerja dengan monorepo menjadi jauh lebih produktif.
Salah satu fitur utama Nx adalah computation caching. Bayangkan Anda menjalankan test suite yang memakan waktu 10 menit. Dengan Nx, jika Anda menjalankan test yang sama tanpa ada perubahan kode, hasilnya akan diambil dari cache dan selesai dalam hitungan detik. Nx cukup pintar untuk mengetahui file mana yang berubah dan hanya menjalankan ulang test yang terpengaruh.
Fitur lainnya adalah dependency graph analysis. Nx memahami hubungan antar proyek dalam monorepo Anda. Jika proyek A menggunakan library B, dan Anda mengubah library B, Nx tahu bahwa proyek A perlu di-rebuild dan di-test ulang. Ini sangat membantu dalam mencegah bug yang muncul akibat perubahan pada shared dependencies.
Nx juga mendukung distributed task execution. Dalam tim besar, Anda bisa mendistribusikan proses build dan testing ke multiple machines, secara dramatis mengurangi waktu CI/CD. Bayangkan monorepo dengan 50 aplikasi yang semuanya perlu di-test. Alih-alih menjalankannya secara sequential yang bisa memakan waktu berjam-jam, Nx bisa mendistribusikan task tersebut dan menyelesaikannya dalam beberapa menit saja.
Nx Monorepo: Kombinasi yang Sempurna
Ketika kita berbicara tentang "Nx Monorepo", kita merujuk pada implementasi monorepo yang menggunakan Nx sebagai build system dan tooling-nya. Ini bukan sekadar menaruh banyak proyek dalam satu folder, tetapi mengorganisirnya dengan cara yang scalable, maintainable, dan performant menggunakan kemampuan Nx.
Dalam Nx Monorepo, kode biasanya diorganisir dalam dua kategori utama: applications (apps) dan libraries (libs). Applications adalah program yang bisa dijalankan secara independen, seperti web server, CLI tool, atau microservice. Libraries adalah kode yang dibagikan antar aplikasi, seperti utility functions, business logic, atau data models.
Struktur ini mendorong pemisahan concerns yang baik. Anda tidak akan menemukan business logic tercampur aduk di dalam aplikasi. Sebaliknya, aplikasi menjadi thin layer yang mengkomposisikan libraries untuk mencapai tujuan tertentu. Ini membuat kode lebih mudah di-test, di-reuse, dan di-maintain.
Cara Kerja dan Keuntungan Praktis
mari kita pahami bagaimana sistem ini bekerja dalam praktik sehari-hari dan apa keuntungannya dibanding pendekatan tradisional.
Workflow Development Sehari-hari
Bayangkan Anda seorang developer yang perlu menambahkan fitur baru: sistem notifikasi email yang akan digunakan oleh user-service untuk mengirim welcome email dan order-service untuk mengirim order confirmation. Dalam pendekatan multi-repo tradisional, Anda perlu membuat package notifikasi terpisah, mempublishnya ke registry, kemudian mengupdate dependencies di kedua service. Prosesnya memakan waktu dan Anda tidak bisa mem-verify bahwa kode Anda bekerja di kedua service sebelum publish.
Dalam Nx Monorepo, workflow-nya jauh lebih smooth. Pertama, Anda membuat library baru di direktori libs/notification dengan kode untuk mengirim email. Kemudian Anda langsung bisa mengimport dan menggunakan library ini di user-service dan order-service. Semua perubahan ada dalam satu commit. Ketika Anda menjalankan test dengan perintah nx test user-service, Nx otomatis mendeteksi bahwa user-service bergantung pada library notification dan akan menjalankan test untuk kedua project. Jika test gagal, Anda langsung tahu ada masalah sebelum commit kode.
Dependency Graph dan Affected Commands
Salah satu fitur paling powerful dari Nx adalah kemampuannya untuk memahami dependency graph proyek Anda. Anda bisa menjalankan perintah nx graph dan Nx akan menampilkan visualisasi interaktif yang menunjukkan bagaimana semua aplikasi dan library dalam monorepo saling berhubungan. Ini sangat membantu ketika Anda join ke proyek baru atau perlu memahami dampak dari perubahan yang akan Anda buat.
Lebih menarik lagi adalah affected commands. Misalkan Anda mengubah library auth untuk menambahkan fitur two-factor authentication. Alih-alih menjalankan test untuk semua 50 aplikasi dalam monorepo Anda, Anda bisa menjalankan nx affected:test dan Nx hanya akan menjalankan test untuk aplikasi yang benar-benar menggunakan library auth. Ini menghemat waktu secara dramatis, terutama dalam monorepo besar. Dalam CI/CD pipeline, ini bisa mengurangi waktu build dari berjam-jam menjadi beberapa menit saja.
Refactoring yang Lebih Aman
Salah satu pain point terbesar dalam pengembangan software adalah melakukan refactoring pada kode yang dibagikan antar banyak aplikasi. Dalam multi-repo, Anda sering kali tidak yakin aplikasi mana saja yang akan terpengaruh oleh perubahan Anda. Anda mungkin melewatkan satu atau dua repository dan deployment mereka akan gagal di production.
Dalam Nx Monorepo, ketika Anda mengubah signature sebuah function di library, IDE Anda akan langsung menunjukkan semua tempat yang perlu diupdate di seluruh monorepo. Anda bisa melakukan refactoring besar-besaran dengan confidence karena semua perubahan terkait ada dalam satu atomic commit. Jika ada yang salah, Anda bisa revert seluruh perubahan dengan satu git revert. Test suite Anda juga akan langsung memberitahu jika ada breaking change yang Anda lewatkan.
Code Sharing yang Natural
Dalam contoh kode kita, perhatikan bagaimana user-service dan product-service keduanya menggunakan library database, logger, dan models. Mereka berbagi kode yang sama persis tanpa perlu khawatir tentang version mismatch. Jika Anda meningkatkan performa connection pooling di library database, semua services langsung mendapat benefit-nya dalam deployment berikutnya.
Ini juga mendorong standarisasi dalam tim. Ketika semua services menggunakan logger yang sama, log format Anda konsisten di seluruh sistem. Ketika semua services menggunakan auth library yang sama, Anda tahu persis bahwa JWT validation bekerja identik di mana-mana. Ini membuat debugging dan monitoring jauh lebih mudah.
Best Practices dan Tips
Setelah memahami konsep dan implementasinya, ada beberapa best practices yang perlu Anda perhatikan untuk memaksimalkan manfaat dari Nx Monorepo.
Organisasi Libraries yang Baik
Jangan tergoda untuk membuat satu giant library yang berisi segalanya. Sebaliknya, pecah libraries berdasarkan domain atau functionality yang jelas. Auth library hanya menghandle authentication dan authorization. Database library hanya menghandle connection dan query utilities. Models library hanya berisi data structures. Pemisahan ini membuat dependency graph lebih clean dan memudahkan reasoning tentang dampak perubahan.
Pertimbangkan juga untuk membuat layers dalam libraries Anda. Misalnya, Anda bisa memiliki low-level libraries yang tidak memiliki dependencies eksternal seperti utils, mid-level libraries yang bergantung pada low-level libraries seperti database dan auth, dan high-level libraries seperti business-logic yang mengkomposisikan berbagai libraries lain. Ini menciptakan clear dependency direction dan mencegah circular dependencies.
Testing Strategy
Dalam monorepo, strategi testing Anda harus lebih sophisticated. Setiap library harus memiliki test suite-nya sendiri yang comprehensive. Ini penting karena libraries digunakan oleh banyak aplikasi, jadi bug dalam library bisa mempengaruhi banyak bagian sistem. Aplikasi juga harus memiliki test sendiri, terutama integration tests yang memverifikasi bahwa komposisi berbagai libraries bekerja dengan benar.
Manfaatkan Nx caching untuk mempercepat test execution. Ketika Anda menjalankan test yang sama berkali-kali tanpa ada perubahan kode, Nx akan menggunakan cached results. Ini sangat membantu dalam development loop dimana Anda sering menjalankan test sebelum commit.
Versioning dan Deployment
Meskipun semua kode ada dalam satu repository, Anda tetap perlu memikirkan versioning dan deployment strategy. Setiap aplikasi bisa di-deploy secara independen dengan version-nya sendiri. Anda bisa menggunakan semantic versioning dimana breaking changes di shared library memicu major version bump untuk aplikasi yang menggunakannya.
Untuk deployment, pertimbangkan untuk menggunakan container orchestration seperti Kubernetes. Setiap service bisa di-build menjadi container image terpisah dan di-deploy secara independen, bahkan jika kodenya ada dalam satu monorepo. CI/CD pipeline Anda bisa menggunakan nx affected:build untuk hanya rebuild dan redeploy services yang terpengaruh oleh perubahan tertentu.
Tantangan dan Solusinya
Tidak ada silver bullet dalam software engineering, dan monorepo pun memiliki tantangannya sendiri. Mari kita diskusikan beberapa tantangan umum dan bagaimana mengatasinya.
Repository Size dan Performance
Seiring waktu, monorepo bisa menjadi sangat besar, terutama jika menyimpan banyak assets atau memiliki history git yang panjang. Ini bisa memperlambat operasi git seperti clone dan checkout. Solusinya adalah menggunakan Git features seperti shallow clone dan sparse checkout. Anda juga bisa menggunakan Git LFS untuk large files dan mempertimbangkan untuk mengarsipkan old branches secara regular.
Nx membantu dengan masalah ini melalui intelligent caching dan computation distribution. Bahkan dalam monorepo dengan ratusan projects, Nx hanya akan menjalankan tasks yang benar-benar diperlukan, bukan semua tasks untuk semua projects.
Access Control dan Ownership
Dalam multi-repo, setiap tim bisa memiliki repository mereka sendiri dengan access control yang granular. Dalam monorepo, semua orang memiliki akses ke semua kode. Ini bisa menjadi concern untuk organisasi besar. Solusinya adalah menggunakan CODEOWNERS file di GitHub untuk mendefinisikan ownership dan mengharuskan review dari owner sebelum merge. Anda juga bisa menggunakan branch protection rules dan required checks untuk memastikan quality standards.
Nx mendukung code ownership concepts melalui tags system. Anda bisa memberi tag pada projects dan membuat lint rules yang enforce bahwa code dari satu scope tidak bisa depend on code dari scope lain tanpa explicit permission.
Onboarding Developer Baru
Monorepo besar bisa overwhelming untuk developer baru. Mereka perlu memahami struktur keseluruhan, dependency relationships, dan tooling yang digunakan. Investasi dalam dokumentasi sangat penting. Buat architecture decision records yang menjelaskan mengapa struktur tertentu dipilih. Maintain up-to-date README di setiap project yang menjelaskan purposenya dan bagaimana cara menjalankannya. Gunakan Nx graph visualization untuk membantu developer baru memahami system architecture secara visual.
Kesimpulan
Monorepo dengan Nx menawarkan cara yang powerful untuk mengelola multiple projects dan shared code dalam satu repository terpusat. Melalui implementasi Golang yang kita bahas, Anda bisa melihat bagaimana konsep ini diterapkan dalam praktik nyata, dari struktur direktori hingga kode implementation yang konkret.
Keuntungan utama dari pendekatan ini adalah code sharing yang natural, refactoring yang lebih aman, atomic changes, dan visibility yang lebih baik terhadap keseluruhan codebase. Dengan Nx sebagai build system, Anda mendapat intelligent caching, affected computation, dan dependency graph analysis yang membuat bekerja dengan monorepo besar tetap efficient dan productive.
Tentu saja, monorepo bukanlah solusi untuk semua kasus. Untuk proyek kecil atau tim yang sangat independent, multi-repo mungkin lebih cocok. Namun untuk organisasi yang memiliki banyak services yang saling related, banyak shared code, atau ingin meningkatkan collaboration antar tim, Nx Monorepo adalah pilihan yang sangat compelling.
Kunci kesuksesan dengan monorepo adalah tooling yang baik, best practices yang consistent, dan investment dalam infrastructure dan dokumentasi. Dengan foundation yang solid seperti yang telah kita bahas, Anda bisa membangun sistem yang scalable dan maintainable yang akan bertahan dalam jangka panjang.
Sekarang giliran Anda untuk mencoba. Mulai dengan monorepo kecil, eksplorasi Nx features, dan rasakan sendiri bagaimana workflow development Anda menjadi lebih smooth dan productive. Selamat mencoba!
Praktek
Bagian 1: Persiapan Environment
Sebelum kita mulai membangun monorepo, kita perlu memastikan semua tools yang diperlukan sudah terinstall di komputer Anda. Mari kita lakukan persiapan ini langkah demi langkah.
Menginstall Golang
Pertama-tama, Anda memerlukan Golang versi 1.21 atau lebih tinggi. Untuk menginstallnya, kunjungi website resmi Golang di https://golang.org/dl/ dan download installer sesuai sistem operasi Anda. Setelah download selesai, jalankan installer dan ikuti instruksinya.
Setelah instalasi selesai, buka terminal dan verifikasi bahwa Golang sudah terinstall dengan benar dengan menjalankan perintah berikut:
go versionAnda seharusnya melihat output yang menampilkan versi Golang yang terinstall, misalnya "go version go1.25.0 darwin/amd64". Jika perintah ini tidak ditemukan, Anda mungkin perlu menambahkan Go ke PATH environment variable Anda. Biasanya installer akan melakukan ini secara otomatis, tetapi jika tidak berhasil, Anda perlu menambahkan direktori instalasi Go ke PATH secara manual.
Menginstall Node.js dan npm
Meskipun kita membangun aplikasi Golang, Nx sendiri adalah tool yang berbasis Node.js, jadi kita perlu menginstall Node.js dan npm (Node Package Manager). Kunjungi https://nodejs.org/ dan download versi LTS (Long Term Support) yang stabil. Versi 18 atau lebih tinggi sangat direkomendasikan.
Setelah instalasi, verifikasi dengan menjalankan:
node --version
npm --versionKedua perintah ini seharusnya menampilkan nomor versi. Jika berhasil, itu berarti Node.js dan npm sudah siap digunakan.
Menginstall Nx CLI
Sekarang kita akan menginstall Nx CLI secara global di sistem Anda. Nx CLI adalah command-line tool yang akan kita gunakan untuk mengelola monorepo. Jalankan perintah berikut di terminal:
npm install -g nxPerintah ini akan menginstall Nx secara global sehingga Anda bisa menggunakannya dari direktori mana pun. Proses instalasi mungkin memakan waktu beberapa menit tergantung kecepatan internet Anda. Setelah selesai, verifikasi instalasi dengan:
nx --versionAnda seharusnya melihat nomor versi Nx yang terinstall.
Menginstall Git
Git adalah version control system yang akan kita gunakan untuk mengelola kode. Kebanyakan sistem sudah memiliki Git terinstall, tetapi jika belum, download dari https://git-scm.com/downloads dan install sesuai sistem operasi Anda.
Verifikasi instalasi Git dengan:
git --versionMenginstall PostgreSQL
Untuk contoh aplikasi kita, kita akan menggunakan PostgreSQL sebagai database. Anda bisa download PostgreSQL dari https://www.postgresql.org/download/ atau menggunakan package manager sistem Anda.
Untuk macOS menggunakan Homebrew:
brew install postgresql@15
brew services start postgresql@15Untuk Ubuntu/Debian:
sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresqlSetelah PostgreSQL berjalan, buat database untuk aplikasi kita:
psql postgres
CREATE DATABASE ecommerce;
\qMenginstall Tools Tambahan (Opsional tapi Direkomendasikan)
Untuk pengalaman development yang lebih baik, install juga tools berikut:
Pertama, golangci-lint untuk linting kode Go:
# Untuk macOS
brew install golangci-lint
# Untuk Linux
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/binKedua, install Visual Studio Code atau IDE favorit Anda dengan extension untuk Go dan Nx.
Bagian 2: Membuat Struktur Monorepo
Sekarang setelah semua tools terinstall, mari kita mulai membuat struktur monorepo dari nol. Proses ini akan kita lakukan secara bertahap agar Anda memahami setiap langkah.
Inisialisasi Project
Pertama, buat direktori untuk monorepo kita dan masuk ke dalamnya:
mkdir ecommerce-monorepo
cd ecommerce-monorepoInisialisasi Git repository di direktori ini:
git initSekarang kita akan menginisialisasi Nx workspace. Jalankan perintah berikut:
npm init -yPerintah ini akan membuat file package.json yang diperlukan untuk mengelola dependencies Node.js. Sekarang kita install Nx sebagai dev dependency di project ini:
npm install --save-dev nx @nrwl/workspaceProses instalasi akan memakan waktu beberapa menit karena Nx memiliki banyak dependencies. Setelah selesai, Anda akan melihat folder node_modules dan file package-lock.json di direktori project Anda.
Membuat Struktur Direktori
Sekarang mari kita buat struktur folder untuk monorepo kita. Kita akan membuat folder apps untuk aplikasi dan folder libs untuk shared libraries:
mkdir -p apps/user-service
mkdir -p apps/product-service
mkdir -p apps/order-service
mkdir -p apps/api-gateway
mkdir -p libs/database
mkdir -p libs/auth
mkdir -p libs/models
mkdir -p libs/logger
mkdir -p libs/utils
mkdir -p tools/scripts
mkdir -p docsPerintah mkdir dengan flag -p akan membuat semua parent directories yang diperlukan secara otomatis. Setelah menjalankan perintah ini, Anda akan memiliki struktur folder dasar untuk monorepo.
Membuat Nx Configuration Files
Sekarang kita perlu membuat file konfigurasi Nx. Pertama, buat file nx.json di root direktori:
cat > nx.json << 'EOF'
{
"extends": "nx/presets/npm.json",
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"],
"parallel": 3
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"dependsOn": ["build"],
"cache": true
}
},
"affected": {
"defaultBase": "main"
}
}
EOFFile ini mendefinisikan konfigurasi global untuk Nx workspace. Kita mengatur bahwa operasi build, test, dan lint bisa di-cache untuk meningkatkan performa. Kita juga mengatur bahwa maksimal 3 tasks bisa berjalan parallel.
Inisialisasi Go Workspace
Go versi 1.18 ke atas mendukung fitur workspace yang sempurna untuk monorepo. Mari kita inisialisasi Go workspace:
go work initPerintah ini akan membuat file go.work di root direktori. File ini akan mencatat semua Go modules dalam monorepo kita.
Sekarang kita juga membuat file go.mod di root untuk metadata workspace:
go mod init ecommerce-monorepoMembuat .gitignore
Sebelum kita mulai menulis kode, mari kita buat file .gitignore agar file-file yang tidak perlu tidak masuk ke Git:
cat > .gitignore << 'EOF'
# Node modules
node_modules/
npm-debug.log
yarn-error.log
# Build outputs
dist/
build/
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Go workspace
go.work.sum
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Nx
.nx/cache
EOFBagian 3: Membuat Shared Libraries
Sekarang kita akan mulai membuat shared libraries yang akan digunakan oleh berbagai aplikasi. Kita akan membuat satu per satu dengan penjelasan lengkap.
Membuat Library Models
Library models akan berisi definisi data structures yang dibagikan antar services. Mari kita buat:
cd libs/models
go mod init ecommerce-monorepo/libs/modelsSekarang tambahkan module ini ke Go workspace dengan kembali ke root direktori dan menjalankan:
cd ../..
go work use ./libs/modelsBuat file untuk User model:
cat > libs/models/user.go << 'EOF'
package models
import "time"
// User represents a user in the system
type User struct {
ID string `json:"id" db:"id"`
Email string `json:"email" db:"email"`
Name string `json:"name" db:"name"`
Password string `json:"-" db:"password"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
EOFBuat file untuk Product model:
cat > libs/models/product.go << 'EOF'
package models
import "time"
// Product represents a product in the catalog
type Product struct {
ID string `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Price float64 `json:"price" db:"price"`
Stock int `json:"stock" db:"stock"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
EOFBuat Nx project configuration untuk library ini:
cat > libs/models/project.json << 'EOF'
{
"name": "models",
"root": "libs/models",
"projectType": "library",
"sourceRoot": "libs/models",
"targets": {
"test": {
"executor": "nx:run-commands",
"options": {
"command": "go test -v ./...",
"cwd": "libs/models"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "golangci-lint run",
"cwd": "libs/models"
}
}
},
"tags": ["type:lib", "scope:shared"]
}
EOFMembuat Library Logger
Library logger akan menyediakan structured logging untuk semua services:
cd libs/logger
go mod init ecommerce-monorepo/libs/logger
cd ../..
go work use ./libs/loggerBuat file logger.go:
cat > libs/logger/logger.go << 'EOF'
package logger
import (
"log/slog"
"os"
)
// Logger wraps slog.Logger with additional functionality
type Logger struct {
*slog.Logger
}
// NewLogger creates a new logger with JSON output format
func NewLogger(serviceName string) *Logger {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
logger := slog.New(handler).With(
slog.String("service", serviceName),
)
return &Logger{Logger: logger}
}
EOFBuat project.json:
cat > libs/logger/project.json << 'EOF'
{
"name": "logger",
"root": "libs/logger",
"projectType": "library",
"sourceRoot": "libs/logger",
"targets": {
"test": {
"executor": "nx:run-commands",
"options": {
"command": "go test -v ./...",
"cwd": "libs/logger"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "golangci-lint run",
"cwd": "libs/logger"
}
}
},
"tags": ["type:lib", "scope:shared"]
}
EOFMembuat Library Auth
Library auth akan menangani JWT authentication:
cd libs/auth
go mod init ecommerce-monorepo/libs/auth
go get github.com/golang-jwt/jwt/v5
cd ../..
go work use ./libs/authBuat file jwt.go:
cat > libs/auth/jwt.go << 'EOF'
package auth
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("token has expired")
)
// Claims contains JWT claims
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
// JWTManager manages JWT tokens
type JWTManager struct {
secretKey []byte
tokenDuration time.Duration
}
// NewJWTManager creates a new JWT manager
func NewJWTManager(secretKey string, duration time.Duration) *JWTManager {
return &JWTManager{
secretKey: []byte(secretKey),
tokenDuration: duration,
}
}
// GenerateToken creates a new JWT token
func (m *JWTManager) GenerateToken(userID, email string) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.tokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(m.secretKey)
}
// ValidateToken verifies a JWT token
func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(
tokenString,
&Claims{},
func(token *jwt.Token) (interface{}, error) {
return m.secretKey, nil
},
)
if err != nil {
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, ErrInvalidToken
}
if claims.ExpiresAt.Before(time.Now()) {
return nil, ErrExpiredToken
}
return claims, nil
}
EOFBuat project.json:
cat > libs/auth/project.json << 'EOF'
{
"name": "auth",
"root": "libs/auth",
"projectType": "library",
"sourceRoot": "libs/auth",
"targets": {
"test": {
"executor": "nx:run-commands",
"options": {
"command": "go test -v ./...",
"cwd": "libs/auth"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "golangci-lint run",
"cwd": "libs/auth"
}
}
},
"tags": ["type:lib", "scope:shared"]
}
EOFMembuat Library Database
Library database akan menyediakan connection pooling:
cd libs/database
go mod init ecommerce-monorepo/libs/database
go get github.com/lib/pq
cd ../..
go work use ./libs/databaseBuat file connection.go:
cat > libs/database/connection.go << 'EOF'
package database
import (
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
)
// Config holds database configuration
type Config struct {
Host string
Port int
User string
Password string
DBName string
SSLMode string
}
// NewConnection creates a new database connection with pooling
func NewConnection(cfg Config) (*sql.DB, error) {
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
)
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Configure connection pool
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
// Verify connection
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
EOFBuat project.json:
cat > libs/database/project.json << 'EOF'
{
"name": "database",
"root": "libs/database",
"projectType": "library",
"sourceRoot": "libs/database",
"targets": {
"test": {
"executor": "nx:run-commands",
"options": {
"command": "go test -v ./...",
"cwd": "libs/database"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "golangci-lint run",
"cwd": "libs/database"
}
}
},
"tags": ["type:lib", "scope:shared"]
}
EOFBagian 4: Membuat Aplikasi User Service
Sekarang kita akan membuat aplikasi pertama kita yang menggunakan libraries yang sudah kita buat.
Inisialisasi User Service Module
cd apps/user-service
go mod init ecommerce-monorepo/apps/user-service
cd ../..
go work use ./apps/user-serviceSekarang tambahkan dependencies ke libraries kita di go.mod user-service:
cat > apps/user-service/go.mod << 'EOF'
module ecommerce-monorepo/apps/user-service
go 1.25
require (
ecommerce-monorepo/libs/auth v0.0.0
ecommerce-monorepo/libs/database v0.0.0
ecommerce-monorepo/libs/logger v0.0.0
ecommerce-monorepo/libs/models v0.0.0
)
replace (
ecommerce-monorepo/libs/auth => ../../libs/auth
ecommerce-monorepo/libs/database => ../../libs/database
ecommerce-monorepo/libs/logger => ../../libs/logger
ecommerce-monorepo/libs/models => ../../libs/models
)
EOFPerhatikan bagian replace yang sangat penting. Ini memberitahu Go untuk menggunakan versi lokal dari libraries kita, bukan mencarinya di internet.
Membuat Main Application
Buat file main.go untuk user-service:
cat > apps/user-service/main.go << 'EOF'
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"ecommerce-monorepo/libs/auth"
"ecommerce-monorepo/libs/database"
"ecommerce-monorepo/libs/logger"
"ecommerce-monorepo/libs/models"
)
type UserService struct {
db *sql.DB
jwtManager *auth.JWTManager
logger *logger.Logger
}
func main() {
// Initialize logger
log := logger.NewLogger("user-service")
log.Info("Starting user service...")
// Setup database connection
dbConfig := database.Config{
Host: getEnv("DB_HOST", "localhost"),
Port: 5432,
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", "postgres"),
DBName: "ecommerce",
SSLMode: "disable",
}
db, err := database.NewConnection(dbConfig)
if err != nil {
log.Error("Failed to connect to database", "error", err)
os.Exit(1)
}
defer db.Close()
log.Info("Database connected successfully")
// Setup JWT manager
jwtSecret := getEnv("JWT_SECRET", "my-secret-key-change-in-production")
jwtManager := auth.NewJWTManager(jwtSecret, 24*time.Hour)
service := &UserService{
db: db,
jwtManager: jwtManager,
logger: log,
}
// Setup HTTP routes
http.HandleFunc("/health", service.HealthCheck)
http.HandleFunc("/users", service.CreateUser)
http.HandleFunc("/login", service.Login)
port := getEnv("PORT", "8081")
log.Info(fmt.Sprintf("User service listening on port %s", port))
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Error("Server failed to start", "error", err)
os.Exit(1)
}
}
func (s *UserService) HealthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
"service": "user-service",
})
}
func (s *UserService) CreateUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var user models.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
s.logger.Error("Failed to decode request", "error", err)
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
s.logger.Info("Creating new user", "email", user.Email)
// In production, you would save to database here
user.ID = "user-123"
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (s *UserService) Login(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var credentials struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&credentials); err != nil {
s.logger.Error("Failed to decode request", "error", err)
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// In production, verify credentials against database
s.logger.Info("User login attempt", "email", credentials.Email)
token, err := s.jwtManager.GenerateToken("user-123", credentials.Email)
if err != nil {
s.logger.Error("Failed to generate token", "error", err)
http.Error(w, "Login failed", http.StatusInternalServerError)
return
}
response := map[string]string{
"token": token,
"user_id": "user-123",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
EOFMembuat Nx Project Configuration
Buat project.json untuk user-service:
cat > apps/user-service/project.json << 'EOF'
{
"name": "user-service",
"root": "apps/user-service",
"projectType": "application",
"sourceRoot": "apps/user-service",
"targets": {
"build": {
"executor": "nx:run-commands",
"options": {
"command": "go build -o ../../dist/user-service",
"cwd": "apps/user-service"
}
},
"test": {
"executor": "nx:run-commands",
"options": {
"command": "go test -v ./...",
"cwd": "apps/user-service"
}
},
"serve": {
"executor": "nx:run-commands",
"options": {
"command": "go run main.go",
"cwd": "apps/user-service"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "golangci-lint run",
"cwd": "apps/user-service"
}
}
},
"tags": ["type:app", "scope:user"],
"implicitDependencies": [
"libs/database",
"libs/auth",
"libs/logger",
"libs/models"
]
}
EOFDownload Dependencies
Sekarang download semua Go dependencies:
cd apps/user-service
go mod tidy
cd ../..Bagian 5: Menjalankan dan Testing
Sekarang kita siap untuk menjalankan aplikasi kita!
Menjalankan User Service
Untuk menjalankan user service, gunakan Nx:
nx serve user-serviceAtau jika Anda ingin menjalankan langsung dengan Go:
cd apps/user-service
go run main.goAnda seharusnya melihat log yang menunjukkan service sudah berjalan di port 8081.
Testing dengan curl
Buka terminal baru dan test health check endpoint:
curl http://localhost:8081/healthAnda seharusnya mendapat response:
{"service":"user-service","status":"healthy"}Test create user endpoint:
curl -X POST http://localhost:8081/users \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","name":"John Doe","password":"secret123"}'Test login endpoint:
curl -X POST http://localhost:8081/login \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","password":"secret123"}'Menjalankan Tests dengan Nx
Untuk menjalankan tests untuk specific project:
nx test user-serviceUntuk menjalankan tests untuk semua projects:
nx run-many --target=test --allMelihat Dependency Graph
Salah satu fitur menarik Nx adalah visualisasi dependency graph:
nx graphIni akan membuka browser dan menampilkan grafik interaktif yang menunjukkan bagaimana semua projects dalam monorepo saling berhubungan.
Building Applications
Untuk build user-service:
nx build user-serviceBinary yang dihasilkan akan ada di folder dist/user-service.
Untuk build semua applications:
nx run-many --target=build --projects=user-service,product-serviceBagian 6: Menambahkan Service Baru
Sekarang Anda sudah memahami alurnya, mari kita buat service kedua dengan cepat.
Membuat Product Service
cd apps/product-service
go mod init ecommerce-monorepo/apps/product-service
cd ../..
go work use ./apps/product-serviceBuat go.mod dengan dependencies:
cat > apps/product-service/go.mod << 'EOF'
module ecommerce-monorepo/apps/product-service
go 1.25
require (
ecommerce-monorepo/libs/database v0.0.0
ecommerce-monorepo/libs/logger v0.0.0
ecommerce-monorepo/libs/models v0.0.0
)
replace (
ecommerce-monorepo/libs/database => ../../libs/database
ecommerce-monorepo/libs/logger => ../../libs/logger
ecommerce-monorepo/libs/models => ../../libs/models
)
EOFBuat main.go:
cat > apps/product-service/main.go << 'EOF'
package main
import (
"encoding/json"
"net/http"
"os"
"ecommerce-monorepo/libs/database"
"ecommerce-monorepo/libs/logger"
"ecommerce-monorepo/libs/models"
)
type ProductService struct {
db *sql.DB
logger *logger.Logger
}
func main() {
log := logger.NewLogger("product-service")
log.Info("Starting product service...")
dbConfig := database.Config{
Host: getEnv("DB_HOST", "localhost"),
Port: 5432,
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", "postgres"),
DBName: "ecommerce",
SSLMode: "disable",
}
db, err := database.NewConnection(dbConfig)
if err != nil {
log.Error("Failed to connect to database", "error", err)
os.Exit(1)
}
defer db.Close()
service := &ProductService{
db: db,
logger: log,
}
http.HandleFunc("/health", service.HealthCheck)
http.HandleFunc("/products", service.ListProducts)
port := getEnv("PORT", "8082")
log.Info("Product service listening on port " + port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Error("Server failed", "error", err)
os.Exit(1)
}
}
func (s *ProductService) HealthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
"service": "product-service",
})
}
func (s *ProductService) ListProducts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
products := []models.Product{
{
ID: "prod-1",
Name: "Laptop Gaming",
Description: "High performance laptop",
Price: 15000000,
Stock: 10,
},
{
ID: "prod-2",
Name: "Wireless Mouse",
Description: "Ergonomic mouse",
Price: 250000,
Stock: 100,
},
}
s.logger.Info("Listing products", "count", len(products))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(products)
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
EOFBuat project.json:
cat > apps/product-service/project.json << 'EOF'
{
"name": "product-service",
"root": "apps/product-service",
"projectType": "application",
"sourceRoot": "apps/product-service",
"targets": {
"build": {
"executor": "nx:run-commands",
"options": {
"command": "go build -o ../../dist/product-service",
"cwd": "apps/product-service"
}
},
"test": {
"executor": "nx:run-commands",
"options": {
"command": "go test -v ./...",
"cwd": "apps/product-service"
}
},
"serve": {
"executor": "nx:run-commands",
"options": {
"command": "go run main.go",
"cwd": "apps/product-service"
}
}
},
"tags": ["type:app", "scope:product"],
"implicitDependencies": [
"libs/database",
"libs/logger",
"libs/models"
]
}
EOFDownload dependencies:
cd apps/product-service
go mod tidy
cd ../..Sekarang Anda bisa menjalankan product service:
nx serve product-serviceTest dengan curl:
curl http://localhost:8082/productsBagian 7: Menggunakan Nx Commands
Sekarang mari kita eksplorasi berbagai Nx commands yang sangat berguna untuk development sehari-hari.
Menjalankan Multiple Services Sekaligus
Untuk menjalankan beberapa services secara bersamaan, buka terminal terpisah untuk masing-masing service, atau gunakan tool seperti Tmux atau gunakan npm script.
Tambahkan script di package.json:
cat > package.json << 'EOF'
{
"name": "ecommerce-monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"start:user": "nx serve user-service",
"start:product": "nx serve product-service",
"build:all": "nx run-many --target=build --all",
"test:all": "nx run-many --target=test --all",
"test:affected": "nx affected --target=test",
"build:affected": "nx affected --target=build",
"lint:all": "nx run-many --target=lint --all",
"graph": "nx graph",
"format": "nx format:write",
"format:check": "nx format:check"
},
"devDependencies": {
"nx": "^17.0.0",
"@nrwl/workspace": "^17.0.0"
}
}
EOFSekarang Anda bisa menggunakan npm scripts:
# Menjalankan user service
npm run start:user
# Build semua applications
npm run build:all
# Test semua projects
npm run test:all
# Lihat dependency graph
npm run graphMenggunakan Affected Commands
Salah satu fitur paling powerful dari Nx adalah affected commands. Ini akan menjalankan tasks hanya untuk projects yang terpengaruh oleh perubahan Anda.
Misalnya, jika Anda mengubah library auth, Anda ingin test semua applications yang menggunakannya:
# Lihat project mana yang terpengaruh
nx affected:graph
# Test hanya project yang terpengaruh
nx affected --target=test
# Build hanya project yang terpengaruh
nx affected --target=buildIni sangat menghemat waktu di CI/CD pipeline. Bayangkan monorepo dengan 50 services - Anda tidak perlu build dan test semuanya setiap kali ada perubahan kecil.
Nx Cache
Nx secara otomatis men-cache hasil dari tasks. Jika Anda menjalankan test yang sama dua kali tanpa mengubah kode, hasil kedua akan diambil dari cache:
# Run pertama - akan execute test
nx test user-service
# Run kedua tanpa perubahan - akan ambil dari cache (sangat cepat!)
nx test user-serviceUntuk clear cache:
nx resetFiltering Projects
Anda bisa menjalankan commands untuk specific projects atau projects dengan tags tertentu:
# Run test untuk specific projects
nx run-many --target=test --projects=user-service,product-service
# Run test untuk semua libraries
nx run-many --target=test --projects=tag:type:lib
# Run test untuk semua applications
nx run-many --target=test --projects=tag:type:appBagian 8: Environment Variables dan Configuration
Untuk mengelola environment variables dengan baik, mari kita buat file .env:
cat > .env.example << 'EOF'
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=ecommerce
# JWT Configuration
JWT_SECRET=change-this-in-production-to-a-secure-random-string
# Service Ports
USER_SERVICE_PORT=8081
PRODUCT_SERVICE_PORT=8082
ORDER_SERVICE_PORT=8083
API_GATEWAY_PORT=8080
EOFCopy ke .env untuk development:
cp .env.example .envTambahkan .env ke .gitignore (seharusnya sudah ada):
echo ".env" >> .gitignoreUntuk load environment variables saat development, Anda bisa menggunakan library seperti godotenv. Install di masing-masing service:
cd apps/user-service
go get github.com/joho/godotenv
cd ../..Kemudian update main.go untuk load .env:
import (
"github.com/joho/godotenv"
)
func main() {
// Load .env file
if err := godotenv.Load("../../.env"); err != nil {
log.Warn("No .env file found, using system environment variables")
}
// rest of your code...
}Bagian 9: Database Migrations
Mari kita setup database migrations untuk mengelola schema database.
Install Migration Tool
Install golang-migrate:
# macOS
brew install golang-migrate
# Linux
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/local/bin/migrate
# Verify
migrate -versionMembuat Migration Scripts
Buat folder untuk migrations:
mkdir -p tools/migrationsBuat migration untuk users table:
migrate create -ext sql -dir tools/migrations -seq create_users_tableEdit file migration up:
cat > tools/migrations/000001_create_users_table.up.sql << 'EOF'
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
EOFEdit file migration down:
cat > tools/migrations/000001_create_users_table.down.sql << 'EOF'
DROP TABLE IF EXISTS users;
EOFBuat migration untuk products:
migrate create -ext sql -dir tools/migrations -seq create_products_tablecat > tools/migrations/000002_create_products_table.up.sql << 'EOF'
CREATE TABLE IF NOT EXISTS products (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(12,2) NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_products_name ON products(name);
EOFcat > tools/migrations/000002_create_products_table.down.sql << 'EOF'
DROP TABLE IF EXISTS products;
EOFMenjalankan Migrations
Buat script untuk menjalankan migrations:
cat > tools/scripts/migrate.sh << 'EOF'
#!/bin/bash
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_USER=${DB_USER:-postgres}
DB_PASSWORD=${DB_PASSWORD:-postgres}
DB_NAME=${DB_NAME:-ecommerce}
DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable"
case "$1" in
up)
echo "Running migrations up..."
migrate -path tools/migrations -database "$DATABASE_URL" up
;;
down)
echo "Running migrations down..."
migrate -path tools/migrations -database "$DATABASE_URL" down
;;
force)
echo "Forcing version $2..."
migrate -path tools/migrations -database "$DATABASE_URL" force "$2"
;;
version)
migrate -path tools/migrations -database "$DATABASE_URL" version
;;
*)
echo "Usage: $0 {up|down|force|version}"
exit 1
;;
esac
EOFBuat executable:
chmod +x tools/scripts/migrate.shJalankan migrations:
./tools/scripts/migrate.sh upTambahkan script ke package.json:
{
"scripts": {
"migrate:up": "./tools/scripts/migrate.sh up",
"migrate:down": "./tools/scripts/migrate.sh down",
"migrate:version": "./tools/scripts/migrate.sh version"
}
}Bagian 10: Docker Setup untuk Development
Mari kita setup Docker untuk memudahkan development environment.
Membuat Docker Compose
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: ecommerce-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ecommerce
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
user-service:
build:
context: .
dockerfile: apps/user-service/Dockerfile
container_name: user-service
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAME: ecommerce
JWT_SECRET: your-secret-key-here
PORT: 8081
ports:
- "8081:8081"
depends_on:
postgres:
condition: service_healthy
product-service:
build:
context: .
dockerfile: apps/product-service/Dockerfile
container_name: product-service
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAME: ecommerce
PORT: 8082
ports:
- "8082:8082"
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
EOFMembuat Dockerfile untuk Services
Buat Dockerfile untuk user-service:
cat > apps/user-service/Dockerfile << 'EOF'
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Copy go workspace files
COPY go.work go.work
COPY go.mod go.mod
# Copy all modules
COPY libs/ libs/
COPY apps/user-service/ apps/user-service/
# Build the application
WORKDIR /app/apps/user-service
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/user-service
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/user-service .
EXPOSE 8081
CMD ["./user-service"]
EOFBuat Dockerfile untuk product-service:
cat > apps/product-service/Dockerfile << 'EOF'
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.work go.work
COPY go.mod go.mod
COPY libs/ libs/
COPY apps/product-service/ apps/product-service/
WORKDIR /app/apps/product-service
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/product-service
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/product-service .
EXPOSE 8082
CMD ["./product-service"]
EOFMenjalankan dengan Docker Compose
# Start semua services
docker-compose up -d
# Lihat logs
docker-compose logs -f
# Stop semua services
docker-compose down
# Rebuild dan restart
docker-compose up -d --buildBagian 11: Testing dan Quality Assurance
Membuat Unit Tests
Buat test untuk auth library:
cat > libs/auth/jwt_test.go << 'EOF'
package auth
import (
"testing"
"time"
)
func TestJWTManager_GenerateToken(t *testing.T) {
manager := NewJWTManager("test-secret", 1*time.Hour)
token, err := manager.GenerateToken("user-123", "test@example.com")
if err != nil {
t.Fatalf("Failed to generate token: %v", err)
}
if token == "" {
t.Error("Token should not be empty")
}
}
func TestJWTManager_ValidateToken(t *testing.T) {
manager := NewJWTManager("test-secret", 1*time.Hour)
// Generate token
token, err := manager.GenerateToken("user-123", "test@example.com")
if err != nil {
t.Fatalf("Failed to generate token: %v", err)
}
// Validate token
claims, err := manager.ValidateToken(token)
if err != nil {
t.Fatalf("Failed to validate token: %v", err)
}
if claims.UserID != "user-123" {
t.Errorf("Expected user ID 'user-123', got '%s'", claims.UserID)
}
if claims.Email != "test@example.com" {
t.Errorf("Expected email 'test@example.com', got '%s'", claims.Email)
}
}
func TestJWTManager_ExpiredToken(t *testing.T) {
manager := NewJWTManager("test-secret", -1*time.Hour) // Expired
token, err := manager.GenerateToken("user-123", "test@example.com")
if err != nil {
t.Fatalf("Failed to generate token: %v", err)
}
_, err = manager.ValidateToken(token)
if err != ErrExpiredToken {
t.Errorf("Expected ErrExpiredToken, got %v", err)
}
}
EOFJalankan tests:
# Test specific library
nx test auth
# Test specific service
nx test user-service
# Test semua
nx run-many --target=test --allSetup Linting dengan golangci-lint
Buat file konfigurasi golangci-lint di root:
cat > .golangci.yml << 'EOF'
linters:
enable:
- gofmt
- govet
- errcheck
- staticcheck
- unused
- gosimple
- ineffassign
- typecheck
linters-settings:
govet:
check-shadowing: true
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
run:
timeout: 5m
tests: true
EOFJalankan linting:
# Lint specific project
nx lint user-service
# Lint semua
nx run-many --target=lint --allBagian 12: CI/CD Pipeline
Membuat GitHub Actions Workflow
Buat folder untuk GitHub Actions:
bash
mkdir -p .github/workflowsBuat workflow file:
cat > .github/workflows/ci.yml << 'EOF'
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
lint-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ecommerce
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Derive appropriate SHAs for base and head
uses: nrwl/nx-set-shas@v3
- name: Run affected lint
run: npx nx affected --target=lint --parallel=3
- name: Run affected tests
run: npx nx affected --target=test --parallel=3
env:
DB_HOST: localhost
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAME: ecommerce
- name: Run affected builds
run: npx nx affected --target=build --parallel=3
EOFBagian 13: Troubleshooting Common Issues
Issue: Import Errors
Jika Anda mendapat error "package not found":
# Clear Go cache
go clean -modcache
# Update go.work
go work sync
# Download dependencies
go mod download
# Tidy up dependencies
go mod tidyIssue: Nx Cache Problems
Jika Nx cache menyebabkan masalah:
# Clear Nx cache
nx reset
# Clear all caches
rm -rf .nx/cache
rm -rf node_modules/.cacheIssue: Port Already in Use
Jika port sudah digunakan:
# Find process using port 8081
lsof -i :8081
# Kill process
kill -9 <PID>
# Or use different port
PORT=8091 nx serve user-serviceKesimpulan
Sekarang Anda memiliki Nx Monorepo Golang yang fully functional dengan:
Last updated