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 version

Anda 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 --version

Kedua 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 nx

Perintah 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 --version

Anda 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 --version

Menginstall 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@15

Untuk Ubuntu/Debian:

sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql

Setelah PostgreSQL berjalan, buat database untuk aplikasi kita:

psql postgres
CREATE DATABASE ecommerce;
\q

Menginstall 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)/bin

Kedua, 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-monorepo

Inisialisasi Git repository di direktori ini:

git init

Sekarang kita akan menginisialisasi Nx workspace. Jalankan perintah berikut:

npm init -y

Perintah 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/workspace

Proses 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 docs

Perintah 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"
  }
}
EOF

File 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 init

Perintah 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-monorepo

Membuat .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
EOF

Bagian 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/models

Sekarang tambahkan module ini ke Go workspace dengan kembali ke root direktori dan menjalankan:

cd ../..
go work use ./libs/models

Buat 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"`
}
EOF

Buat 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"`
}
EOF

Buat 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"]
}
EOF

Membuat 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/logger

Buat 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}
}
EOF

Buat 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"]
}
EOF

Membuat 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/auth

Buat 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
}
EOF

Buat 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"]
}
EOF

Membuat 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/database

Buat 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
}
EOF

Buat 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"]
}
EOF

Bagian 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-service

Sekarang 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
)
EOF

Perhatikan 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
}
EOF

Membuat 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"
  ]
}
EOF

Download 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-service

Atau jika Anda ingin menjalankan langsung dengan Go:

cd apps/user-service
go run main.go

Anda 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/health

Anda 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-service

Untuk menjalankan tests untuk semua projects:

nx run-many --target=test --all

Melihat Dependency Graph

Salah satu fitur menarik Nx adalah visualisasi dependency graph:

nx graph

Ini 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-service

Binary yang dihasilkan akan ada di folder dist/user-service.

Untuk build semua applications:

nx run-many --target=build --projects=user-service,product-service

Bagian 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-service

Buat 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
)
EOF

Buat 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
}
EOF

Buat 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"
  ]
}
EOF

Download dependencies:

cd apps/product-service
go mod tidy
cd ../..

Sekarang Anda bisa menjalankan product service:

nx serve product-service

Test dengan curl:

curl http://localhost:8082/products

Bagian 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"
  }
}
EOF

Sekarang 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 graph

Menggunakan 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=build

Ini 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-service

Untuk clear cache:

nx reset

Filtering 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:app

Bagian 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
EOF

Copy ke .env untuk development:

cp .env.example .env

Tambahkan .env ke .gitignore (seharusnya sudah ada):

echo ".env" >> .gitignore

Untuk 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 -version

Membuat Migration Scripts

Buat folder untuk migrations:

mkdir -p tools/migrations

Buat migration untuk users table:

migrate create -ext sql -dir tools/migrations -seq create_users_table

Edit 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);
EOF

Edit file migration down:

cat > tools/migrations/000001_create_users_table.down.sql << 'EOF'
DROP TABLE IF EXISTS users;
EOF

Buat migration untuk products:

migrate create -ext sql -dir tools/migrations -seq create_products_table
cat > 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);
EOF
cat > tools/migrations/000002_create_products_table.down.sql << 'EOF'
DROP TABLE IF EXISTS products;
EOF

Menjalankan 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
EOF

Buat executable:

chmod +x tools/scripts/migrate.sh

Jalankan migrations:

./tools/scripts/migrate.sh up

Tambahkan 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:
EOF

Membuat 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"]
EOF

Buat 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"]
EOF

Menjalankan 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 --build

Bagian 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)
    }
}
EOF

Jalankan tests:

# Test specific library
nx test auth

# Test specific service
nx test user-service

# Test semua
nx run-many --target=test --all

Setup 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
EOF

Jalankan linting:

# Lint specific project
nx lint user-service

# Lint semua
nx run-many --target=lint --all

Bagian 12: CI/CD Pipeline

Membuat GitHub Actions Workflow

Buat folder untuk GitHub Actions:

bash

mkdir -p .github/workflows

Buat 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
EOF

Bagian 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 tidy

Issue: Nx Cache Problems

Jika Nx cache menyebabkan masalah:

# Clear Nx cache
nx reset

# Clear all caches
rm -rf .nx/cache
rm -rf node_modules/.cache

Issue: 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-service

Kesimpulan

Sekarang Anda memiliki Nx Monorepo Golang yang fully functional dengan:

Last updated