A Go toolkit for building HTTP services with Gin. Provides configuration management, database access (MySQL/PostgreSQL), JWT authentication, structured logging, middleware, and standardized API responses out of the box.
Looking for a working example? Check out cetus-demo — a complete REST API with user registration, JWT auth, and CRUD, built step by step.
- Configuration - Environment-based config with
.envfiles, singleton pattern - Database - MySQL & PostgreSQL via GORM, auto driver selection
- JWT Authentication - RSA-signed tokens with Redis-backed session management
- Logging - Structured logging with Zap (JSON/console, file/stdout)
- Middleware - Request ID tracking, rate limiting
- API Responses - Standardized JSON response helpers for Gin
- Utilities - Bcrypt hashing, ID obfuscation (Optimus), RSA crypto, i18n, file operations
go get github.com/JackDPro/cetusCopy the example and modify values for your environment:
cp .env.example .envKey environment variables:
APP_NAME=my-app
APP_ENV=dev
LOG_CONSOLE_OUT=true
LOG_FILE_OUT=false
LOG_LEVEL=debug
LOG_FORMAT=json
DB_TYPE=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=mydb
DB_USERNAME=root
DB_PASSWORD=password
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DATABASE=0
REDIS_PASSWORD=password
SERVER_HTTP_PORT=80
SERVER_GRPC_PORT=50051See .env.example for all available options.
package main
import (
"fmt"
"github.com/JackDPro/cetus/config"
"github.com/JackDPro/cetus/controller"
"github.com/JackDPro/cetus/middleware"
"github.com/JackDPro/cetus/provider"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.New()
router.Use(gin.Recovery())
// CORS
corsConf := cors.DefaultConfig()
corsConf.AllowAllOrigins = true
corsConf.AllowHeaders = []string{"Authorization", "Accept-Language"}
router.Use(cors.New(corsConf))
// Request ID middleware
router.Use(middleware.RequestId())
// Health check endpoint
probeCtr := controller.NewProbeController()
router.GET("/probe", probeCtr.Show)
// Your routes here
// router.GET("/users", userController.Index)
conf := config.GetApiConfig()
addr := fmt.Sprintf("0.0.0.0:%d", conf.HttpPort)
provider.GetLogger().Info("server starting", "address", addr)
if err := router.Run(addr); err != nil {
panic(err)
}
}All configuration is loaded from environment variables (via .env file). Each config struct uses the singleton pattern.
import "github.com/JackDPro/cetus/config"
appConf := config.GetAppConfig() // App name, env, data root, public URL
dbConf := config.GetDatabaseConfig() // DB type, host, port, credentials
apiConf := config.GetApiConfig() // HTTP and gRPC ports
authConf := config.GetAuthConf() // JWT cert/key paths, expiration
redisConf := config.GetRedisConfig() // Redis connection info
logConf := config.GetLogConfig() // Log level, format, output targets
hashConf := config.GetHashIdConfig() // Optimus ID hashing params
natsConf := config.GetNatsConf() // NATS messaging configUse APP_ENV to control behavior:
if config.GetAppConfig().Env == "prod" {
gin.SetMode(gin.ReleaseMode)
}Thread-safe singleton providers for common infrastructure.
Structured logging powered by Zap. Supports console/JSON format, file/stdout output.
import "github.com/JackDPro/cetus/provider"
logger := provider.GetLogger()
logger.Info("server started")
logger.Infow("user created", "userId", 123)
logger.Errorw("request failed", "error", err)GORM wrapper with automatic MySQL/PostgreSQL driver selection based on DB_TYPE.
import "github.com/JackDPro/cetus/provider"
db := provider.GetOrm().Db
// Use standard GORM operations
var users []User
db.Where("active = ?", true).Find(&users)
// Auto-migrate
db.AutoMigrate(&User{}, &Post{})import "github.com/JackDPro/cetus/provider"
rdb := provider.GetRedisClient()
rdb.Set(ctx, "key", "value", time.Hour)
val, err := rdb.Get(ctx, "key").Result()import "github.com/JackDPro/cetus/provider"
// Hash a password
hashed, err := provider.HashMake("my-password")
// Verify
err = provider.HashCheck("my-password", hashed)
// Check if re-hash is needed
if provider.HashNeedRefresh(hashed) {
// re-hash with current cost
}Reversible integer ID encoding to hide sequential database IDs in APIs.
import "github.com/JackDPro/cetus/provider"
encoded := provider.Hash().Encode(42) // e.g. 1580030173
decoded := provider.Hash().Decode(1580030173) // 42Requires OPTIMUS_PRIME, OPTIMUS_INVERSE, OPTIMUS_RANDOM in .env.
How to get these values:
- Visit http://primes.utm.edu/lists/small/millions/, download any
.txtfile, open it and randomly pick a prime number less than2,147,483,647— this will be yourOPTIMUS_PRIME. - Use the generator in cetus-demo to calculate the other two values:
go run storage/optimus_gen.go 104393867
# Output:
# OPTIMUS_PRIME=104393867
# OPTIMUS_INVERSE=1990279033
# OPTIMUS_RANDOM=1333095938Important: Once deployed to production, never change these values — all existing encoded IDs will become invalid.
import "github.com/JackDPro/cetus/provider"
// Load from file
rsaKey, err := provider.NewRsaByKeyFile("path/to/private.pem")
// Sign data
signature, err := rsaKey.Signature(data)
// Verify signature
err = rsaKey.VerifySignature(data, signature)import "github.com/JackDPro/cetus/provider"
provider.RandomString(16) // "aB3kF9mNpQ2xR7wL"
provider.RandomInt(6) // "483921"
provider.StringInSlice("a", []string{"a", "b"}) // trueimport "github.com/JackDPro/cetus/provider"
provider.CopyFile("src.txt", "dst.txt")
provider.CopyDir("src_dir", "dst_dir") // recursive copyimport "github.com/JackDPro/cetus/provider"
t := provider.GetTranslate()
t.SetLanguage("zh")
msg := t.Tr("hello_world")
// Or use the global shorthand
msg := provider.Tr("hello_world")Embed BaseModel in your models to get serialization helpers:
import "github.com/JackDPro/cetus/model"
type User struct {
model.BaseModel
Id uint64 `json:"id" gorm:"primaryKey"`
Nickname string `json:"nickname"`
Email string `json:"email"`
}
// Implement IModel interface
func (u *User) ToMap() (map[string]interface{}, error) {
return u.BaseModel.ToMap(u)
}BaseModel provides:
ToMap(model)- Convert struct to map usingjsontagsToJson(model)- Convert struct to JSON stringInclude(model, relations, db)- Preload GORM relations from a comma-separated string
Any model implementing IModel can be used with the response helpers:
type IModel interface {
ToMap() (map[string]interface{}, error)
}// API responses are wrapped in DataWrapper
type DataWrapper struct {
Data interface{} `json:"data"`
Meta interface{} `json:"meta,omitempty"`
}
// Error response
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
}
// Pagination metadata
type Pagination struct {
Count int `json:"count"`
CurrentPage int `json:"current_page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
}Standardized HTTP response functions for Gin handlers:
import "github.com/JackDPro/cetus/controller"
func (ctr *UserController) Store(c *gin.Context) {
// Validate input
if err := c.ShouldBindJSON(&req); err != nil {
controller.ResponseUnprocessable(c, 1, "invalid params", err)
return
}
// Create resource
user, err := createUser(req)
if err != nil {
controller.ResponseInternalError(c, 2, "create failed", err)
return
}
// Return created resource
controller.ResponseCreated(c, user.Id)
}
func (ctr *UserController) Show(c *gin.Context) {
user, err := findUser(id)
if err != nil {
controller.ResponseNotFound(c, "user not found")
return
}
controller.ResponseItem(c, user) // user must implement IModel
}
func (ctr *UserController) Index(c *gin.Context) {
users, meta := listUsers(page, perPage)
controller.ResponseCollection(c, users, meta)
}Available response helpers:
| Function | HTTP Status | Use Case |
|---|---|---|
ResponseSuccess(c) |
200 | Action succeeded |
ResponseItem(c, item) |
200 | Return single resource |
ResponseCollection(c, items, meta) |
200 | Return list with optional pagination |
ResponseCreated(c, id) |
201 | Resource created (sets Location header) |
ResponseAccepted(c) |
202 | Async action accepted |
ResponseBadRequest(c, code, msg) |
400 | Invalid request |
ResponseUnauthorized(c) |
401 | Authentication required |
ResponseForbidden(c) |
403 | Permission denied |
ResponseNotFound(c, msg) |
404 | Resource not found |
ResponseUnprocessable(c, code, msg, err) |
422 | Validation failed |
ResponseInternalError(c, code, msg, err) |
500 | Server error (auto-logged) |
RSA-signed JWT tokens with Redis-backed session storage. Supports access tokens and refresh tokens.
Prerequisites:
- RSA key pair (PKCS#8 private key + PEM public certificate)
- Redis server
- JWT config in
.env
JWT_EXPIRES_IN=72
JWT_REDIS_PREFIX=auth
JWT_CERT_PATH=storage/jwt.crt
JWT_KEY_PATH=storage/jwt.key
JWT_ISSUE=example.comGenerate RSA keys:
The private key must be in PKCS#8 DER format, the public key in PEM format:
# 1. Generate RSA private key (PKCS#1 PEM)
openssl genrsa -out jwt1.pem 2048
# 2. Convert to PKCS#8 DER format (required by cetus)
openssl pkcs8 -topk8 -inform PEM -outform DER \
-in jwt1.pem -out storage/jwt.key -nocrypt
# 3. Extract public key (PEM)
openssl rsa -in jwt1.pem -pubout -out storage/jwt.crt
# 4. Clean up
rm jwt1.pemUsage:
import "github.com/JackDPro/cetus/jwt"
guard, err := jwt.GetJwtGuard()
// Create access token + refresh token
accessToken, err := guard.CreateToken(userId, true) // true = revoke old tokens
// accessToken.AccessToken - JWT string
// accessToken.RefreshToken - Refresh JWT string
// accessToken.Type - "bearer"
// accessToken.ExpiresIn - Expiration in seconds
// Create access token only (e.g. for API keys)
accessToken, err := guard.CreateAccessToken(userId)
// Validate token
validToken, err := guard.Attempt(tokenString)
// validToken.UserId - Authenticated user ID
// validToken.Type - "token", "refresh", or "access_key"
// Revoke token (logout)
err = guard.DeleteCredential(tokenString)Adds a unique request ID to every request. Checks incoming headers first (X-Request-Id, HTTP_X_REQUEST_ID, HTTP_REQUEST_ID), generates a UUID if none found.
import "github.com/JackDPro/cetus/middleware"
router.Use(middleware.RequestId())Powered by tollbooth.
import (
"github.com/JackDPro/cetus/middleware"
"github.com/didip/tollbooth/v7/limiter"
)
lmt := limiter.New(nil).SetMax(100) // 100 requests per second
router.Use(middleware.LimitRate(lmt))Use BaseModel.Include() to dynamically preload relations from query parameters:
// GET /users?include=posts,comments
func (ctr *UserController) Index(c *gin.Context) {
includeStr := c.Query("include")
var users []User
db := provider.GetOrm().Db
db = (&model.BaseModel{}).Include(&User{}, includeStr, db)
db.Find(&users)
controller.ResponseCollection(c, users, nil)
}A complete REST API setup:
package main
import (
"fmt"
"github.com/JackDPro/cetus/config"
"github.com/JackDPro/cetus/controller"
"github.com/JackDPro/cetus/middleware"
"github.com/JackDPro/cetus/provider"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
appConf := config.GetAppConfig()
if appConf.Env == "prod" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Recovery())
// CORS
corsConf := cors.DefaultConfig()
corsConf.AllowAllOrigins = true
corsConf.AllowHeaders = []string{"Authorization", "Accept-Language"}
router.Use(cors.New(corsConf))
// Middleware
router.Use(middleware.RequestId())
// Public routes
probeCtr := controller.NewProbeController()
router.GET("/probe", probeCtr.Show)
router.POST("/auth/login", authLogin)
router.POST("/users", createUser)
// Protected routes (add your auth middleware)
authorized := router.Group("/")
// authorized.Use(yourAuthMiddleware())
authorized.GET("/users/me", getMe)
authorized.POST("/auth/logout", logout)
// Start
apiConf := config.GetApiConfig()
addr := fmt.Sprintf("0.0.0.0:%d", apiConf.HttpPort)
provider.GetLogger().Info("server starting", "address", addr)
if err := router.Run(addr); err != nil {
panic(err)
}
}Use bcrypt hashing in GORM model hooks for automatic password encryption:
import (
"github.com/JackDPro/cetus/model"
"github.com/JackDPro/cetus/provider"
"gorm.io/gorm"
)
type User struct {
model.BaseModel
Id uint64 `json:"id" gorm:"primaryKey"`
Email string `json:"email"`
Password string `json:"password,omitempty"`
}
func (u *User) BeforeSave(tx *gorm.DB) (err error) {
if u.Password != "" {
u.Password, err = provider.HashMake(u.Password)
}
return
}
func (u *User) ToMap() (map[string]interface{}, error) {
return u.BaseModel.ToMap(u)
}| Variable | Description | Default/Example |
|---|---|---|
APP_NAME |
Application name | my-app |
APP_ENV |
Environment (dev/prod) |
dev |
APP_DATA_ROOT |
Data storage root path | /usr/app |
APP_PUBLIC_RES_URL |
Public static resource URL | http://example.com/static |
LOG_CONSOLE_OUT |
Output logs to console | true |
LOG_FILE_OUT |
Output logs to file | false |
LOG_FILE_PATH |
Log file path | /var/log/app.log |
LOG_LEVEL |
Log level (debug/info/warn/error) |
debug |
LOG_FORMAT |
Log format (json/console) |
json |
DB_TYPE |
Database type (mysql/postgres) |
mysql |
DB_HOST |
Database host | 127.0.0.1 |
DB_PORT |
Database port | 3306 |
DB_DATABASE |
Database name | mydb |
DB_USERNAME |
Database username | root |
DB_PASSWORD |
Database password | - |
DB_SSLMODE |
PostgreSQL SSL mode | disable |
DB_MIGRATE_SELF_ONLY |
Limit migration scope | false |
JWT_EXPIRES_IN |
Token expiration (hours) | 72 |
JWT_REDIS_PREFIX |
Redis key prefix for tokens | auth |
JWT_CERT_PATH |
RSA public key path | storage/jwt.crt |
JWT_KEY_PATH |
RSA private key path | storage/jwt.key |
JWT_ISSUE |
JWT issuer claim | example.com |
OPTIMUS_PRIME |
Optimus prime number | - |
OPTIMUS_INVERSE |
Optimus inverse | - |
OPTIMUS_RANDOM |
Optimus random seed | - |
REDIS_HOST |
Redis host | 127.0.0.1 |
REDIS_PORT |
Redis port | 6379 |
REDIS_DATABASE |
Redis database index | 0 |
REDIS_PASSWORD |
Redis password | - |
NATS_HOST |
NATS server host | 127.0.0.1 |
NATS_USERNAME |
NATS username | - |
NATS_PASSWORD |
NATS password | - |
SERVER_HTTP_PORT |
HTTP server port | 80 |
SERVER_GRPC_PORT |
gRPC server port | 50051 |
See cetus-demo for a complete working example with user registration, JWT authentication, and CRUD operations.
MIT