pkg

module
v0.5.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 13, 2026 License: MIT

README

Go Package Collection

Go Reference Test Go Report Card

A collection of production-ready Go packages for building web services: database, Redis (standard + cluster), JWT, crypto, GCS, structured logging, HTTP middleware, OpenTelemetry (otelx), Sentry (sentryx), mutual TLS (mtls), Prometheus metrics, graceful shutdown, and utilities. Follows clean architecture boundaries — domain, use-case, and infrastructure are separate.

Contents


Installation

go get github.com/turahe/pkg

Requires Go 1.26+.


Packages

config

Loads all configuration from environment variables (or a .env file via godotenv). All packages read from the global Configuration singleton.

API:

config.Setup(configPath string) error   // load .env; "" uses env vars only
config.GetConfig() *Configuration       // get global config (builds from env if needed)
config.SetConfig(cfg *Configuration)    // override config (testing / manual wiring)

Configuration struct (abbreviated):

type Configuration struct {
    Server      ServerConfiguration
    Cors        CorsConfiguration
    Database    DatabaseConfiguration
    DatabaseSite DatabaseConfiguration  // optional second DB
    Redis       RedisConfiguration
    GCS         GCSConfiguration
    RateLimiter RateLimiterConfiguration
    Timezone    TimezoneConfiguration
    OpenTelemetry OpenTelemetryConfiguration
    Sentry        SentryConfiguration
    MTLS          MTLSConfiguration
}

database

Enterprise-grade GORM database layer. Supports MySQL, Postgres, SQLite, SQL Server, and Google Cloud SQL (Postgres/MySQL) with IAM auth and Private IP. Includes SQL redaction in logs (passwords, tokens, card numbers).

Drivers: mysql · postgres · sqlite · sqlserver · cloudsql-mysql · cloudsql-postgres

New API (recommended — dependency injection):

db, err := database.New(&cfg.Database, database.Options{
    LogLevel: logger.Warn,
})
// or with Cloud SQL options:
db, err := database.New(&cfg.Database, database.Options{
    UseIAM:       true,
    UsePrivateIP: true,
    LogLevel:     logger.Warn,
})

gormDB  := db.DB()                    // *gorm.DB
err     = db.Health(ctx)             // ping with PingTimeout-bounded context
err     = db.Close()                  // close connections + Cloud SQL connector

Options:

Option Default Description
MaxOpenConns 30 Max open DB connections
MaxIdleConns 10 Max idle DB connections
ConnMaxLife 30 min Max connection lifetime
ConnMaxIdle 10 min Max connection idle time
SlowThreshold 500 ms GORM slow query log threshold
PingTimeout 5 s Health check ping timeout
LogLevel Warn GORM log level
UseIAM false Cloud SQL IAM auth
UsePrivateIP false Cloud SQL Private IP
EnableOpenTelemetry auto GORM OTel plugin; auto when tracing enabled (OTEL_GORM_ENABLED)

Connection timezone: set DATABASE_TIMEZONE (IANA name) or leave empty to use SERVER_TIMEZONE (default UTC). Applied to MySQL loc, Postgres timezone, and Cloud SQL drivers.

OpenTelemetry GORM: when otelx.Init succeeds (OTLP or GCP exporter), GORM spans are registered automatically unless disabled with database.WithOpenTelemetry(false).

High-RPS pool preset:

db, err := database.New(&cfg.Database, database.Options{}, database.WithProductionPoolDefaults)
// sets MaxOpenConns=150, MaxIdleConns=50

Legacy API (global, backward-compatible):

database.Setup()          // init from config env vars
database.GetDB()          // *gorm.DB (panics if not initialized)
database.GetDBSite()      // secondary *gorm.DB (falls back to primary)
database.HealthCheck(ctx) // check both primary and site DB
database.IsAlive()        // bool
database.Cleanup()        // close all connections

redis

Redis client wrapper for both standalone Redis and Redis Cluster (Google Cloud Memorystore). Connection pooling, timeouts, and all common commands included.

Setup:

redis.Setup() error          // reads config; no-op if REDIS_ENABLED=false
redis.Close() error          // close client; call on shutdown
redis.IsAlive() bool         // ping check
redis.GetUniversalClient() redis.Cmdable  // works with both modes
redis.GetRedis() *redis.Client            // standard mode only
redis.GetRedisCluster() *redis.ClusterClient  // cluster mode only

String operations:

redis.Get(key string) (string, error)
redis.Set(key string, value interface{}, expiration time.Duration) error
redis.Delete(keys ...string) error
redis.MGet(keys ...string) ([]interface{}, error)
redis.MSet(pairs ...interface{}) error

Hash operations:

redis.HGet(key, field string) (string, error)
redis.HGetAll(key string) (map[string]string, error)
redis.HSet(key, field string, value interface{}) error
redis.HSetMap(key string, fields map[string]interface{}) error

List operations:

redis.LPush(key string, values ...interface{}) error
redis.RPop(key string) (string, error)
redis.LRange(key string, start, stop int64) ([]string, error)

Set operations:

redis.SAdd(key string, members ...interface{}) error
redis.SMembers(key string) ([]string, error)
redis.SRem(key string, members ...interface{}) error

Distributed lock:

redis.AcquireLock(key string, expiration time.Duration) (bool, error)
redis.ExtendLock(key string, expiration time.Duration) (bool, error)
redis.ReleaseLock(key string) error

Pipeline, Pub/Sub, Scan:

redis.Pipeline(fn func(redis.Pipeliner) error) error
redis.PipelineSet(pairs map[string]interface{}, expiration time.Duration) error
redis.PublishMessage(channel string, message interface{}) error
redis.SubscribeToChannel(channel string) *redis.PubSub
redis.ScanKeys(pattern string, count int64) ([]string, error)  // cluster-aware

logger

Structured logging built on log/slog. Outputs Google Cloud Logging-compatible JSON with severity, time, message, trace_id, correlation_id, sourceLocation, and optional fields. Logs to stderr (no log file). The underlying writer is lazy-initialized on first log write (no I/O at startup).

Plain functions:

logger.Debugf(format string, args ...interface{})
logger.Infof(format string, args ...interface{})
logger.Warnf(format string, args ...interface{})
logger.Errorf(format string, args ...interface{})
logger.Fatalf(format string, args ...interface{})  // calls os.Exit(1)

Structured fields:

logger.Debug(msg string, fields logger.Fields)
logger.Info(msg string, fields logger.Fields)
logger.Warn(msg string, fields logger.Fields)
logger.Error(msg string, fields logger.Fields)

Context-bound (includes trace_id / correlation_id automatically):

// Store IDs in context (done by RequestID / TraceMiddleware):
ctx = logger.WithTraceID(ctx, traceID)
ctx = logger.WithCorrelationID(ctx, correlationID)

// Read IDs from context:
logger.GetTraceID(ctx)
logger.GetCorrelationID(ctx)

// Context-bound logger — all log calls carry the IDs from ctx:
log := logger.WithContext(ctx)
log.Infof("user %s logged in", userID)
log.Errorf("operation failed: %v", err)

// Or use context-aware free functions:
logger.InfofContext(ctx, "processed %d records", n)
logger.ErrorfContext(ctx, "failed: %v", err)

Configuration:

logger.SetLogLevel(slog.LevelDebug)  // default: Info
logger.GetLogger() *slog.Logger
logger.GetWriter() io.Writer         // stderr (used e.g. by GORM logger)

middlewares

Gin middleware collection. Designed to be composed in a specific order for correct behaviour.

Recommended middleware order:

router := gin.New()

// Create JWT verifier (Manager for sign+verify, or Verifier for verify-only)
jwtManager, err := jwt.NewManager(context.Background(), config.GetConfig())
if err != nil {
    log.Fatal(err)
}
// Or for API-only services: jwtVerifier, _ := jwt.NewVerifier(ctx, config.GetConfig())

router.Use(
    middlewares.RecoveryHandler,              // 1. catch panics first
    middlewares.RequestID(),                  // 2. inject trace/correlation IDs
    middlewares.LoggerMiddleware(),            // 3. log with IDs in context
    middlewares.Metrics(),                    // 4. Prometheus instrumentation
    middlewares.RequestTimeout(10*time.Second), // 5. bound all downstream handlers
    middlewares.CORS(),                       // 6. CORS headers (incl. X-2FA-Code)
    middlewares.MTLSMiddleware(),           // 7. client cert check (when MTLS_ENABLED)
    middlewares.AuthMiddleware(jwtManager),   // 8. JWT auth (pass *Manager or *Verifier)
    middlewares.RateLimiter(),                // 9. rate limit (requires Redis)
)
router.NoMethod(middlewares.NoMethodHandler())
router.NoRoute(middlewares.NoRouteHandler())

Reference:

Middleware Signature Description
RecoveryHandler gin.HandlerFunc Catches panics; returns structured JSON 500 with stack trace in logs
RequestID() gin.HandlerFunc Reads X-Request-ID / X-Trace-ID or generates UUID; injects into context and response headers
TraceMiddleware() gin.HandlerFunc Reads X-Trace-Id, X-Correlation-Id, X-Request-Id separately; use when upstream sends distinct trace and correlation IDs
LoggerMiddleware() gin.HandlerFunc Structured request log (method, path, status, latency, IP, user-agent, trace IDs)
Metrics() gin.HandlerFunc Prometheus counters, histogram, and in-flight gauge; uses route pattern to avoid high-cardinality labels
RequestTimeout(d) gin.HandlerFunc Adds context.WithTimeout to every request; no-op when d <= 0
CORS() gin.HandlerFunc CORS headers; global or per-origin (CORS_FRONTEND / CORS_IPS) from config
AuthMiddleware(verifier) jwt.TokenVerifiergin.HandlerFunc Validates Bearer JWT; sets user_id, original_user_id, is_impersonating in context. Pass *jwt.Manager or *jwt.Verifier.
MTLSMiddleware() gin.HandlerFunc Requires verified TLS client cert when MTLS_ENABLED; sets mtls_client_cn in context
RateLimiter() gin.HandlerFunc Redis Lua single-round-trip rate limiter; sets X-RateLimit-* headers; supports IP or user keying and skip-paths
NoMethodHandler() gin.HandlerFunc 405 JSON response
NoRouteHandler() gin.HandlerFunc 404 JSON response

Prometheus metrics exposed by Metrics():

Metric Type Labels
http_requests_total Counter method, path, status
http_request_duration_seconds Histogram method, path, status
http_requests_in_flight Gauge

Register the scrape endpoint separately:

import "github.com/prometheus/client_golang/prometheus/promhttp"
router.GET("/metrics", gin.WrapH(promhttp.Handler()))

otelx

OpenTelemetry trace setup wired to config.OpenTelemetry. Supports OTLP HTTP or Google Cloud Trace (OTEL_TRACES_EXPORTER=gcp).

import "github.com/turahe/pkg/otelx"

shutdown, enabled := otelx.Init(ctx, config.GetConfig().OpenTelemetry)
if enabled {
    defer shutdown(ctx)
}

// GORM instrumentation (auto when tracing enabled + OTEL_GORM_ENABLED=true)
db, _ := database.New(&cfg.Database, database.Options{})

API:

Function Description
otelx.LoadConfig() Returns config.OpenTelemetryConfiguration
otelx.Init(ctx, cfg) Configures global TracerProvider; returns shutdown func + enabled flag
otelx.RegisterGORM(db, opts) Manual GORM OTel plugin registration
otelx.TracingEnabled(cfg) True when OTLP endpoint or gcp exporter is configured

GCP mode: set OTEL_TRACES_EXPORTER=gcp and GOOGLE_CLOUD_PROJECT (or OTEL_GCP_PROJECT_ID). Uses Application Default Credentials and enables X-Cloud-Trace-Context propagation automatically.


sentryx

Sentry error reporting wired to config.Sentry. Empty SENTRY_DSN disables Sentry with zero overhead.

import "github.com/turahe/pkg/sentryx"

if sentryx.Init(config.GetConfig().Sentry) {
    defer sentryx.Flush(config.GetConfig().Sentry.FlushTimeout)
}
Function Description
sentryx.LoadConfig() Returns config.SentryConfiguration
sentryx.Init(cfg) Initializes Sentry; returns enabled flag
sentryx.Flush(timeout) Drains buffered events on shutdown

mtls

Mutual TLS helpers for backend HTTPS (require client cert) and gateway outbound connections. Config from config.MTLS.

Server (backend):

import "github.com/turahe/pkg/mtls"

srv := &http.Server{Addr: ":" + cfg.Server.Port, Handler: router}
_ = mtls.ConfigureServer(srv)
go mtls.ListenConfigured(srv)

router.Use(middlewares.MTLSMiddleware()) // HTTP-layer client cert enforcement

Client (gateway outbound):

transport, err := mtls.NewTransport(config.GetConfig().MTLS)
client := &http.Client{Transport: transport}
Function Description
mtls.LoadConfig() Returns config.MTLSConfiguration
mtls.ServerTLSConfig(cfg) TLS config with RequireAndVerifyClientCert
mtls.ClientTLSConfig(cfg) TLS config with client cert for outbound calls
mtls.ConfigureServer(srv) Applies server TLS when MTLS_ENABLED
mtls.ListenConfigured(srv) ListenAndServeTLS or plain HTTP
mtls.RunGin(engine, addr) Gin drop-in for engine.Run with optional mTLS
mtls.NewTransport(cfg) http.Transport for gateway upstream HTTPS

When MTLS_ENABLED=false, all helpers fall back to plain HTTP.


handler

Base handler for Gin with binding, pagination, error routing, and role checks.

type BaseHandler struct{}

// Bind request body / query / URI by content-type and method.
func (c *BaseHandler) ValidateReqParams(ctx *gin.Context, params interface{}) error

// 422 validation error response (Laravel-style field map).
func (c *BaseHandler) HandleValidationError(ctx *gin.Context, serviceCode string, err error)

// Clamp page/size to defaults (1, 10) and max (100).
func (c *BaseHandler) NormalizePagination(pageNumber, pageSize int) (int, int)

// Extract and validate URL param; writes 422 and returns false if missing.
func (c *BaseHandler) GetIDFromParam(ctx *gin.Context, paramName, serviceCode string) (string, bool)

// Prefer reqID; fall back to URL param.
func (c *BaseHandler) GetIDFromRequestOrParam(ctx *gin.Context, reqID, paramName, serviceCode string) (string, bool)

// Route domain errors to correct HTTP status. Returns true if handled.
// Checks errors.Is(ErrNotFound) → 404, errors.Is(ErrUnauthorized) → 401,
// then notFoundMessages list, then falls back to 500.
func (c *BaseHandler) HandleServiceError(ctx *gin.Context, serviceCode string, err error, notFoundMessages ...string) bool

// Build SimplePaginationResponse from a slice, page info, and total.
func (c *BaseHandler) BuildPaginationResponse(data []interface{}, pageNumber, pageSize int, total int64) response.SimplePaginationResponse

// Read "user_id" string from Gin context (set by AuthMiddleware).
func (c *BaseHandler) GetCurrentUserID(ctx *gin.Context) (string, bool)

// Return true if any userRole matches any requiredRole.
func (c *BaseHandler) CheckUserHasRole(userRoles, requiredRoles []string) bool

response

Standardised JSON responses with a 7-digit composite code (HTTP(3) + Service(2) + Case(2)).

Success responses:

response.Ok(ctx)
response.OkWithMessage(ctx, message)
response.OkWithData(ctx, data)
response.Created(ctx, data)
response.Updated(ctx, data)
response.Deleted(ctx)

Error responses:

response.FailWithMessage(ctx, message)
response.FailWithDetailed(ctx, httpStatus, serviceCode, caseCode, data, message)
response.ValidationError(ctx, serviceCode, err)          // 422 with field map
response.ValidationErrorSimple(ctx, serviceCode, field, message)
response.NotFoundError(ctx, serviceCode, caseCode, message)   // 404
response.UnauthorizedError(ctx, message)                       // 401
response.ForbiddenError(ctx, message)                          // 403
response.ConflictError(ctx, serviceCode, caseCode, message)    // 409

Pagination responses:

response.SimplePaginated(ctx, data, pageNumber, pageSize, hasNext, hasPrev)
response.CursorPaginated(ctx, data, nextCursor, hasNext)

Service codes: ServiceCodeCommon, ServiceCodeAuth, ServiceCodeTransaction, ServiceCodeWithdrawal, ServiceCodeUser, ServiceCodeAdmin, ServiceCodeMerchant, ServiceCodeSetting, ServiceCodeRole, ServiceCodePermission, ServiceCodeNotification, ServiceCodeIPWhitelist, ServiceCodeApiKey, ServiceCodeDeposit, ServiceCodeWallet, ServiceCodePhone, ServiceCodeEmail, ServiceCodeTwoFactor, ServiceCodeBank, ServiceCodeBankAccount, and more. Case codes cover success, validation, auth, not-found, business logic, server errors, conflicts, email/phone change, 2FA, and bank/bank-account flows (see response/codes.go).


jwt

JWT token generation and validation with HS256, RS256 (default), or ES256. Keys are loaded from config (env, file paths, or embedded PEM). No global state: use Manager (sign + verify), Signer (issue tokens only), or Verifier (validate only). Supports issuer, audience, key ID (kid), and token type (access, refresh, impersonation).

All-in-one (single service):

manager, err := jwt.NewManager(ctx, config.GetConfig())
if err != nil {
    log.Fatal(err)
}
router.Use(middlewares.AuthMiddleware(manager))

token, _ := manager.GenerateToken(userID)
claims, _ := manager.ValidateToken(tokenString)

Split services (auth server issues, API server only verifies):

// Auth/login service — needs private key or secret
signer, err := jwt.NewSigner(ctx, config.GetConfig())
token, _ := signer.GenerateToken(userID)
refresh, _ := signer.GenerateRefreshToken(userID)
impersonation, _ := signer.GenerateImpersonationToken(adminID, "admin", targetID, 15*time.Minute)

// API/gateway service — needs only public key or secret
verifier, err := jwt.NewVerifier(ctx, config.GetConfig())
router.Use(middlewares.AuthMiddleware(verifier))  // TokenVerifier interface
claims, _ := verifier.ValidateToken(tokenString)

API summary:

Symbol Description
jwt.NewManager(ctx, cfg) All-in-one; loads sign + verify keys; returns error
jwt.NewSigner(ctx, cfg) Signing only (private key or secret)
jwt.NewVerifier(ctx, cfg) Verification only (public key or secret)
jwt.TokenVerifier Interface: ValidateToken(string) (*Claims, error); implemented by *Manager and *Verifier
manager.GenerateToken(id) Access token (default expiry)
manager.GenerateTokenWithExpiry(id, expiry) Access token with custom expiry
manager.GenerateRefreshToken(id) Refresh token
manager.GenerateImpersonationToken(adminID, role, targetID, ttl) Short-lived impersonation token (max 30 min)
manager.ValidateToken(token) / verifier.ValidateToken(token) Parse and verify; return *Claims or error
jwt.ComparePassword(hashed, plain) bcrypt comparison
jwt.GetCurrentUserUUID(ctx) Read user_id from Gin context (set by AuthMiddleware)

Config / env: Default algorithm is RS256. For HS256 set JWT_SIGNING_ALGORITHM=HS256 and SERVER_SECRET. For RS256/ES256 set JWT_PRIVATE_KEY and JWT_PUBLIC_KEY (file path or inline PEM), or embed keys and assign to config before calling NewManager/NewSigner/NewVerifier. Optional: JWT_ISSUER, JWT_AUDIENCE, JWT_KEY_ID.

Embed keys (no file paths): Put PEM files in a package (e.g. keys/) and embed them; then set config so the JWT package uses the embedded bytes instead of paths:

//go:embed keys/private.pem
var privateKeyPEM []byte

//go:embed keys/public.pem
var publicKeyPEM []byte

func main() {
    cfg := config.GetConfig()
    cfg.Server.JWTPrivateKeyPEM = privateKeyPEM
    cfg.Server.JWTPublicKeyPEM = publicKeyPEM
    manager, err := jwt.NewManager(ctx, cfg)
    // ...
}

crypto

bcrypt password hashing.

crypto.HashAndSalt(plainPassword []byte) string
crypto.ComparePassword(hashedPassword string, plainPassword []byte) bool

gcs

Google Cloud Storage wrapper. Uses Application Default Credentials (ADC) or an explicit service account JSON file.

gcs.Setup() error
gcs.GetClient() *storage.Client
gcs.GetBucket() *storage.BucketHandle
gcs.ReadObject(objectName string) ([]byte, error)
gcs.ReadObjectAsReader(objectName string) (io.ReadCloser, error)
gcs.WriteObject(objectName string, data []byte, contentType string) error
gcs.DeleteObject(objectName string) error
gcs.ObjectExists(objectName string) (bool, error)
gcs.ListObjects(prefix string) ([]string, error)
gcs.Close() error

types

Shared types for handlers and repositories (no infrastructure dependencies).

// Conditions is the WHERE clause map passed to repository methods.
type Conditions map[string]interface{}

// PageInfo holds offset-based pagination request (pageNumber, pageSize) with form/json tags.
type PageInfo struct {
    PageNumber int `form:"pageNumber" json:"pageNumber"`
    PageSize   int `form:"pageSize" json:"pageSize"`
}

// TimeRange holds a start/end pair (IANA or RFC3339 strings).
type TimeRange struct {
    Start string `json:"start"`
    End   string `json:"end"`
}

util
util.IsEmpty(value interface{}) bool
util.InAnySlice[T comparable](haystack []T, needle T) bool
util.RemoveDuplicates[T comparable](haystack []T) []T
util.FormatPhoneNumber(phone *string, countryCode *string) *string  // E.164 via nyaruka/phonenumbers

domain

Domain errors and port interfaces. No dependencies on infrastructure.

// Sentinel errors for handlers and use cases (use with errors.Is).
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)
domain/port

Port interfaces implemented by infrastructure (e.g. repositories).

type GetByID interface {
    GetByID(ctx context.Context, id string) (interface{}, bool, error)
}
usecase

Application service layer. Use cases depend only on domain and ports.

type Runner interface { Run(ctx context.Context) error }
type Func func(ctx context.Context) error  // adapts func to Runner

// Example: get item by ID; returns domain.ErrNotFound when not found.
func GetItemByID(ctx context.Context, repo port.GetByID, id string) (interface{}, error)
repositories

GORM-based base repository: CRUD, First, Find, Scan, SimplePagination, RawSQL, ExecSQL. Use with dependency injection (NewBaseRepositoryWithDB) or global DB (NewBaseRepository). Implements IBaseRepository; adapt to domain/port in your app.

repo := repositories.NewBaseRepositoryWithDB(db.DB())
repo.Create(ctx, &model)
repo.First(ctx, &out, types.Conditions{"id = ?": id})
repo.SimplePagination(ctx, &model, &out, page, size, conditions, orders, "User", "Items")

Documentation

All packages follow GoDoc conventions:

  • Each package has a doc.go describing its role, responsibilities, constraints, and what it must not do.
  • Exported symbols have comments that start with the symbol name and describe behavior clearly.
  • Repository, adapter, and domain packages document contracts, error behavior, and context use.

Run go doc or view pkg.go.dev for the full API.


Minimal server
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync/atomic"
    "syscall"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "github.com/turahe/pkg/config"
    "github.com/turahe/pkg/database"
    "github.com/turahe/pkg/middlewares"
    "github.com/turahe/pkg/mtls"
    "github.com/turahe/pkg/otelx"
    pkgredis "github.com/turahe/pkg/redis"
    "github.com/turahe/pkg/sentryx"
    "gorm.io/gorm/logger"
)

func main() {
    if err := config.Setup(""); err != nil {
        log.Fatal(err)
    }
    cfg := config.GetConfig()

    ctx := context.Background()
    otelShutdown, otelEnabled := otelx.Init(ctx, cfg.OpenTelemetry)
    sentryEnabled := sentryx.Init(cfg.Sentry)

    // Database
    db, err := database.New(&cfg.Database, database.Options{LogLevel: logger.Warn})
    if err != nil {
        log.Fatal(err)
    }

    // Redis (optional)
    if cfg.Redis.Enabled {
        if err := pkgredis.Setup(); err != nil {
            log.Printf("redis: %v", err)
        }
    }

    // Readiness gate
    var ready atomic.Bool
    ready.Store(true)

    // Router
    router := gin.New()
    router.Use(
        middlewares.RecoveryHandler,
        middlewares.RequestID(),
        middlewares.LoggerMiddleware(),
        middlewares.Metrics(),
        middlewares.RequestTimeout(10*time.Second),
        middlewares.CORS(),
        middlewares.MTLSMiddleware(),
    )
    router.GET("/metrics", gin.WrapH(promhttp.Handler()))
    router.GET("/live", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })
    router.GET("/ready", func(c *gin.Context) {
        if !ready.Load() {
            c.JSON(http.StatusServiceUnavailable, gin.H{"status": "shutting_down"})
            return
        }
        if err := db.Health(c.Request.Context()); err != nil {
            c.JSON(http.StatusServiceUnavailable, gin.H{"status": "degraded", "error": err.Error()})
            return
        }
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // Application routes ...
    router.NoMethod(middlewares.NoMethodHandler())
    router.NoRoute(middlewares.NoRouteHandler())

    srv := &http.Server{
        Addr:              ":" + cfg.Server.Port,
        Handler:           router,
        ReadHeaderTimeout: 5 * time.Second,
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      10 * time.Second,
        IdleTimeout:       60 * time.Second,
    }
    if err := mtls.ConfigureServer(srv); err != nil {
        log.Fatal(err)
    }
    go func() {
        if err := mtls.ListenConfigured(srv); err != nil && err != http.ErrServerClosed {
            log.Printf("server: %v", err)
        }
    }()

    // Graceful shutdown (25 s matches terminationGracePeriodSeconds: 30)
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
    ready.Store(false) // signal readiness probe → 503 immediately

    shutCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
    defer cancel()
    srv.Shutdown(shutCtx)

    if sentryEnabled {
        sentryx.Flush(cfg.Sentry.FlushTimeout)
    }
    if otelEnabled {
        otelShutdown(shutCtx)
    }

    if cfg.Redis.Enabled {
        pkgredis.Close()
    }
    db.Close()
}
Repository with dependency injection
import (
    "github.com/turahe/pkg/database"
    "github.com/turahe/pkg/repositories"
    "github.com/turahe/pkg/types"
)

db, _ := database.New(&cfg.Database, database.Options{})
repo := repositories.NewBaseRepositoryWithDB(db.DB())

// Create
err := repo.Create(ctx, &myModel)

// Find with conditions
var results []MyModel
err = repo.Find(ctx, &results, types.Conditions{
    "status = ?": "active",
    "tenant_id = ?": tenantID,
})

// Pagination (pass preload names to avoid N+1)
total, err := repo.SimplePagination(ctx, &MyModel{}, &results, page, size,
    types.Conditions{"status = ?": "active"},
    []string{"created_at DESC"},
    "User", "Items",  // preloads
)
Clean architecture wiring
// domain/port/repository.go
type ItemRepository interface {
    GetByID(ctx context.Context, id string) (*Item, bool, error)
}

// infrastructure/repository/item.go
type itemRepo struct{ base repositories.IBaseRepository }

func (r *itemRepo) GetByID(ctx context.Context, id string) (*Item, bool, error) {
    var item Item
    notFound, err := r.base.First(ctx, &item, types.Conditions{"id = ?": id})
    return &item, !notFound, err
}

// usecase/get_item.go
func GetItem(ctx context.Context, repo port.ItemRepository, id string) (*Item, error) {
    item, found, err := repo.GetByID(ctx, id)
    if err != nil { return nil, err }
    if !found { return nil, domain.ErrNotFound }
    return item, nil
}

Environment Variables

Copy .env.example to .env and call config.Setup(""):

cp .env.example .env
Server
Variable Default Description
SERVER_PORT 8080 HTTP listen port
SERVER_SECRET JWT secret for HS256 only; required when JWT_SIGNING_ALGORITHM=HS256
SERVER_MODE debug Gin mode: debug, release, test
SERVER_ACCESS_TOKEN_EXPIRY 1 Access token lifetime (hours)
SERVER_REFRESH_TOKEN_EXPIRY 7 Refresh token lifetime (days)
SERVER_SESSION_EXPIRY 24 Session lifetime (hours)
SERVER_TIMEZONE UTC IANA timezone (also default for DB when DATABASE_TIMEZONE empty)
APP_ENV Deployment environment; fallback for OTEL/Sentry when their env vars are empty
CORS
Variable Default Description
CORS_GLOBAL true Allow all origins (*)
CORS_FRONTEND Frontend origin URL when CORS_GLOBAL=false (e.g. http://localhost:3000)
CORS_IPS Comma-separated allowed origins when CORS_GLOBAL=false

Allowed headers include Authorization, X-CSRF-Token, and X-2FA-Code.

JWT
Variable Default Description
JWT_SIGNING_ALGORITHM RS256 HS256 · RS256 · ES256
JWT_PRIVATE_KEY File path or inline PEM private key (RS256/ES256)
JWT_PUBLIC_KEY File path or inline PEM public key (RS256/ES256)
JWT_ISSUER Issuer (iss) claim
JWT_AUDIENCE Audience (aud), comma-separated
JWT_KEY_ID Key ID (kid) in JWT header

Keys can be embedded via config.Server.JWTPrivateKeyPEM / JWTPublicKeyPEM (see jwt section).

Database
Variable Default Description
DATABASE_DRIVER mysql mysql · postgres · sqlite · sqlserver · cloudsql-mysql · cloudsql-postgres
DATABASE_HOST 127.0.0.1
DATABASE_PORT 3306
DATABASE_USERNAME
DATABASE_PASSWORD
DATABASE_DBNAME
DATABASE_SSLMODE false
DATABASE_LOGMODE false Enable GORM query logging
DATABASE_TIMEZONE IANA timezone for DB session (mysql/postgres/Cloud SQL); empty uses SERVER_TIMEZONE
DATABASE_MAX_IDLE_CONNS 0 (→ 10) Connection pool idle size
DATABASE_MAX_OPEN_CONNS 0 (→ 30) Connection pool max open
DATABASE_CONN_MAX_LIFETIME 0 (→ 30 min) Connection lifetime (minutes)
DATABASE_CLOUD_SQL_INSTANCE project:region:instance for Cloud SQL
DATABASE_*_SITE Same keys with _SITE suffix for secondary DB
DATABASE_TIMEZONE_SITE Site DB timezone; empty uses SERVER_TIMEZONE
Redis
Variable Default Description
REDIS_ENABLED false
REDIS_HOST 127.0.0.1
REDIS_PORT 6379
REDIS_PASSWORD
REDIS_DB 0 Database index (ignored in cluster mode)
REDIS_CLUSTER_MODE false Enable cluster client
REDIS_CLUSTER_NODES Comma-separated host:port list
REDIS_POOL_SIZE 0 (client default) Max connections per node
REDIS_MIN_IDLE_CONNS 0 Min idle connections
REDIS_READ_TIMEOUT_SEC 0 (no timeout) Read timeout
REDIS_WRITE_TIMEOUT_SEC 0 (no timeout) Write timeout
Rate Limiter
Variable Default Description
RATE_LIMITER_ENABLED false Requires Redis
RATE_LIMITER_REQUESTS 100 Requests allowed per window
RATE_LIMITER_WINDOW 60 Window size (seconds)
RATE_LIMITER_KEY_BY ip ip or user
RATE_LIMITER_SKIP_PATHS Comma-separated paths (e.g. /health,/metrics)
GCS
Variable Default Description
GCS_ENABLED false
GCS_BUCKET_NAME
GCS_CREDENTIALS_FILE Path to service account JSON; omit to use ADC
OpenTelemetry
Variable Default Description
OTEL_TRACES_EXPORTER otlp otlp or gcp (Google Cloud Trace via ADC)
OTEL_EXPORTER_OTLP_ENDPOINT OTLP HTTP endpoint; required when exporter is otlp
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT Optional trace-specific endpoint override
OTEL_EXPORTER_OTLP_INSECURE true Use HTTP instead of HTTPS for OTLP export
OTEL_EXPORTER_OTLP_HEADERS Comma-separated key=value OTLP headers
OTEL_SERVICE_NAME Service name resource attribute; otelx defaults to app
OTEL_ENVIRONMENT Deployment environment; falls back to APP_ENV
OTEL_SERVICE_VERSION Service version; falls back to SENTRY_RELEASE
OTEL_TRACES_SAMPLER_ARG 1.0 Parent-based ratio sampler (0..1)
OTEL_SHUTDOWN_TIMEOUT 5s Tracer provider shutdown timeout
OTEL_GORM_ENABLED true Register GORM OTel plugin when tracing is enabled
OTEL_GCP_PROJECT_ID GCP project for Cloud Trace; falls back to GOOGLE_CLOUD_PROJECT
OTEL_GCP_PROPAGATOR false Enable X-Cloud-Trace-Context propagation (auto-on for gcp exporter)

Initialize tracing with otelx.Init(ctx, config.GetConfig().OpenTelemetry) after config.Setup. For Google Cloud Trace set OTEL_TRACES_EXPORTER=gcp. GORM is instrumented automatically when tracing is enabled and OTEL_GORM_ENABLED=true (default).

Sentry
Variable Default Description
SENTRY_DSN Sentry DSN; empty disables Sentry
SENTRY_ENVIRONMENT Deployment environment; falls back to APP_ENV
SENTRY_RELEASE Release identifier (git SHA or semver)
SENTRY_SERVER_NAME Server name tag; sentryx defaults to app
SENTRY_DEBUG false Enable Sentry SDK debug logging
SENTRY_ATTACH_STACKTRACE true Attach stack traces to non-panic messages
SENTRY_SAMPLE_RATE 1.0 Error event sample rate (0..1)
SENTRY_TRACES_SAMPLE_RATE 0.0 Performance transaction sample rate (0..1)
SENTRY_FLUSH_TIMEOUT 2s Flush timeout at shutdown

Initialize Sentry with sentryx.Init(config.GetConfig().Sentry) after config.Setup.

mTLS
Variable Default Description
MTLS_ENABLED false Enable mTLS server listen and MTLSMiddleware
MTLS_CA_CERT /etc/mtls/ca.crt Shared CA bundle
MTLS_SERVER_CERT /etc/mtls/server.crt Backend server certificate
MTLS_SERVER_KEY /etc/mtls/server.key Backend server private key
MTLS_CLIENT_CERT /etc/mtls/gateway.crt Gateway/client certificate (outbound)
MTLS_CLIENT_KEY /etc/mtls/gateway.key Gateway/client private key
MTLS_SKIP_PATHS /live,/ready,/metrics Paths bypassing MTLSMiddleware

See mtls for server/client wiring examples.


Production Wiring Example

See cmd/example/main.go for a complete wiring of:

  • config.Setup and optional otelx / sentryx initialization
  • Dependency-injected database with health check and optional GORM tracing
  • Optional Redis setup and graceful Close()
  • Optional mTLS via mtls.ConfigureServer + MTLSMiddleware
  • gin.New() with explicit middleware stack (recovery → trace → logging → metrics → timeout → CORS → mTLS)
  • /live (liveness), /ready (readiness with component checks), /metrics (Prometheus)
  • HTTP server with all timeouts set
  • Graceful shutdown: readiness gate → srv.Shutdown(25s) → Sentry flush → OTel shutdown → Redis close → DB close
Kubernetes probes
livenessProbe:
  httpGet:
    path: /live
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

terminationGracePeriodSeconds: 30
Docker
docker build -t myapp .
docker run --env-file .env -p 8080:8080 myapp

The multi-stage Dockerfile uses golang:1.26-alpine to build and gcr.io/distroless/base-debian12:nonroot as the runtime image. Runs as UID 65532 (non-root).


Testing

Run all tests:

go test ./...

With race detector (requires CGO):

CGO_ENABLED=1 go test -race ./...

With coverage:

go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
Run tests with Docker Compose

Run the full test suite (including integration tests) without local Go or databases:

make test-docker

Or with docker compose directly:

docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test
docker compose -f docker-compose.test.yml down -v

The test runner waits for Redis, MySQL, and Postgres to be healthy, then runs go test -v -race -count=1 ./.... Coverage is written to coverage.out and a summary is printed.

Integration tests (Redis · MySQL · Postgres) — local

Start services with Docker Compose:

docker compose up -d

Run with environment variables:

REDIS_ENABLED=true REDIS_HOST=127.0.0.1 REDIS_PORT=6379 \
DATABASE_DRIVER=mysql DATABASE_HOST=127.0.0.1 DATABASE_PORT=3306 \
DATABASE_USERNAME=root DATABASE_PASSWORD=root DATABASE_DBNAME=testdb \
go test ./...

Integration tests skip automatically when services are unavailable. CI runs the full matrix with these services via GitHub Actions.

Packages with tests: config, crypto, database, gcs, handler, jwt, logger, middlewares, mtls, otelx, redis, repositories, response, sentryx, types, util.


License

MIT

Directories

Path Synopsis
Package config provides application configuration loaded from environment variables.
Package config provides application configuration loaded from environment variables.
Package crypto provides password hashing and comparison using bcrypt (golang.org/x/crypto/bcrypt).
Package crypto provides password hashing and comparison using bcrypt (golang.org/x/crypto/bcrypt).
Package database provides a GORM-based database layer with support for MySQL, Postgres, SQLite, SQL Server, and Google Cloud SQL (Postgres/MySQL with optional IAM and Private IP).
Package database provides a GORM-based database layer with support for MySQL, Postgres, SQLite, SQL Server, and Google Cloud SQL (Postgres/MySQL with optional IAM and Private IP).
Package domain provides domain errors and port interfaces.
Package domain provides domain errors and port interfaces.
port
Package port defines repository and other ports (interfaces) used by use cases.
Package port defines repository and other ports (interfaces) used by use cases.
Package gcs provides a Google Cloud Storage client wrapper (cloud.google.com/go/storage) configured from config package.
Package gcs provides a Google Cloud Storage client wrapper (cloud.google.com/go/storage) configured from config package.
Package handler provides a base HTTP handler for Gin with binding, validation, error mapping, and pagination helpers.
Package handler provides a base HTTP handler for Gin with binding, validation, error mapping, and pagination helpers.
Package jwt provides JWT token generation and validation with configurable signing: HS256 (symmetric), RS256 (RSA, default), or ES256 (ECDSA).
Package jwt provides JWT token generation and validation with configurable signing: HS256 (symmetric), RS256 (RSA, default), or ES256 (ECDSA).
Package middlewares provides Gin HTTP middleware for cross-cutting concerns: recovery, tracing, logging, metrics, timeout, CORS, auth, and rate limiting.
Package middlewares provides Gin HTTP middleware for cross-cutting concerns: recovery, tracing, logging, metrics, timeout, CORS, auth, and rate limiting.
Package mtls provides mutual TLS helpers for backend HTTPS and gateway outbound clients.
Package mtls provides mutual TLS helpers for backend HTTPS and gateway outbound clients.
Package otelx wraps OpenTelemetry trace setup with configuration from config so the rest of the service doesn't need to know about OTel's surface area.
Package otelx wraps OpenTelemetry trace setup with configuration from config so the rest of the service doesn't need to know about OTel's surface area.
Package redis provides a Redis client wrapper (github.com/redis/go-redis/v9) for standalone and cluster modes, configured from config package.
Package redis provides a Redis client wrapper (github.com/redis/go-redis/v9) for standalone and cluster modes, configured from config package.
Package repositories provides a GORM-based base repository implementing generic CRUD, pagination, and raw SQL with context support.
Package repositories provides a GORM-based base repository implementing generic CRUD, pagination, and raw SQL with context support.
Package response provides standardized JSON response types and helpers using a 7-digit composite code (HTTP_STATUS + SERVICE_CODE + CASE_CODE).
Package response provides standardized JSON response types and helpers using a 7-digit composite code (HTTP_STATUS + SERVICE_CODE + CASE_CODE).
Package sentryx wraps github.com/getsentry/sentry-go with configuration from config so the rest of the service doesn't need to know about Sentry's surface area.
Package sentryx wraps github.com/getsentry/sentry-go with configuration from config so the rest of the service doesn't need to know about Sentry's surface area.
Package types provides shared types used across handler, repository, and use-case layers.
Package types provides shared types used across handler, repository, and use-case layers.
Package usecase provides the application service layer: use cases depend only on domain (errors and port interfaces).
Package usecase provides the application service layer: use cases depend only on domain (errors and port interfaces).
Package util provides generic helpers for common operations: empty checks, slice membership, deduplication, and phone formatting.
Package util provides generic helpers for common operations: empty checks, slice membership, deduplication, and phone formatting.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL