Skip to content

Repository files navigation

Go Server Starter

δΈ­ζ–‡ζ–‡ζ‘£

A production-ready Go web server boilerplate/starter kit with clean architecture, built-in authentication, RBAC, and comprehensive tooling.

✨ Features

  • Web Framework: Gin - High-performance HTTP web framework
  • Database: MySQL with GORM ORM, auto-migration support
  • Cache: Redis integration
  • Authentication: JWT-based auth with multi-device token expiration support
  • Authorization: Role-Based Access Control (RBAC)
  • Validation: Request validation via go-playground/validator
  • Internationalization: i18n support (English & Chinese)
  • Logging: Structured logging with Zap + log rotation with Lumberjack
  • Configuration: Environment-based config management with Viper
  • Async Tasks: Background job processing with Asynq
  • ID Generation: Distributed ID generation with Snowflake
  • Graceful Shutdown: Clean server shutdown handling
  • Clean Architecture: Layered structure (Handler β†’ Service β†’ Repository)

πŸ“ Project Structure

go-server-starter/
β”œβ”€β”€ cmd/
β”‚   └── server/              # Application entry point
β”œβ”€β”€ configs/                 # Configuration files
β”‚   β”œβ”€β”€ config.yml           # Default config (no secrets)
β”‚   β”œβ”€β”€ config.dev.yml       # Local dev overrides (gitignored)
β”‚   β”œβ”€β”€ config.dev.yml.example  # Dev config template
β”‚   └── config.test.yml      # Test config (gitignored)
β”œβ”€β”€ docs/                    # Swagger generated docs (committed)
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ app/                 # Application initialization & DI
β”‚   β”œβ”€β”€ config/              # Config structs & loading (koanf v2)
β”‚   β”œβ”€β”€ constant/            # Constants (Redis keys, context keys)
β”‚   β”œβ”€β”€ ctx/                 # Custom request context (Gin wrapper)
β”‚   β”œβ”€β”€ database/
β”‚   β”‚   └── migration/       # Goose migrations (embedded SQL)
β”‚   β”‚   └── migrate.go       # Goose migrations entry point
β”‚   β”œβ”€β”€ dto/                 # Data Transfer Objects
β”‚   β”œβ”€β”€ enum/                # Enumerations
β”‚   β”œβ”€β”€ exception/           # Domain exceptions with i18n
β”‚   β”œβ”€β”€ handler/             # HTTP handlers (controllers)
β”‚   β”œβ”€β”€ i18n/                # Internationalization
β”‚   β”œβ”€β”€ middleware/          # HTTP middlewares
β”‚   β”œβ”€β”€ model/               # Database models (GORM)
β”‚   β”œβ”€β”€ repo/                # Repository layer (data access)
β”‚   β”œβ”€β”€ router/              # Route definitions
β”‚   β”œβ”€β”€ seed/                # Database seeders
β”‚   └── service/             # Business logic layer
β”œβ”€β”€ pkg/
β”‚   β”œβ”€β”€ auth/                # RBAC middleware
β”‚   β”œβ”€β”€ cronjob/             # Cron job scheduler
β”‚   β”œβ”€β”€ database/            # MySQL connection & auto-create DB
β”‚   β”œβ”€β”€ jwt/                 # JWT token management
β”‚   β”œβ”€β”€ logger/              # Zap logger + Lumberjack rotation
β”‚   β”œβ”€β”€ notify/              # Email/SMS verify code Notification service
β”‚   β”œβ”€β”€ redis/               # Redis client
β”‚   β”œβ”€β”€ snowflake/           # Snowflake ID generator
β”‚   β”œβ”€β”€ taskq/               # Asynq client/server
β”‚   β”œβ”€β”€ translator/          # Validator translator (zh/en)
β”‚   β”œβ”€β”€ utils/               # Common utilities
β”‚   β”œβ”€β”€ validator/           # Custom validation rules
β”‚   └── verify_code/         # Verify Email/SMS verify code
β”œβ”€β”€ .air.toml                # Air hot reload config
β”œβ”€β”€ .env.example             # Environment variables template
β”œβ”€β”€ CLAUDE.md                # Claude Code guidance
β”œβ”€β”€ Dockerfile               # Multi-stage build
β”œβ”€β”€ docker-compose.yml       # Production Docker Compose
β”œβ”€β”€ docker-compose.dev.yml   # Dev Docker Compose (MySQL + Redis only)
β”œβ”€β”€ generate.sh              # Entity scaffold generator
β”œβ”€β”€ Makefile                 # Build and run tasks
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
└── logs/                    # Log files

πŸš€ Getting Started

Prerequisites

  • Go 1.21+
  • MySQL 8.0+
  • Redis 6.0+

Installation

  1. Clone the repository:
git clone https://github.com/your-username/go-server-starter.git
cd go-server-starter
  1. Install dependencies:
go mod download
  1. Configure the application:

Copy and modify the config file for your environment:

cp configs/config.yml configs/config.dev.yml

Edit configs/config.dev.yml with your database and Redis settings.

  1. Run the server:
# Development mode
go run cmd/server/server.go -mode=dev

# Production mode
go run cmd/server/server.go -mode=prod

# Test mode
go run cmd/server/server.go -mode=test

The server will start on http://localhost:8080 by default.

βš™οΈ Configuration

Configuration is managed via YAML files. Key settings include:

server:
  port: 8080
  readTimeout: 10s
  writeTimeout: 10s
  apiPrefix: "/api"

jwt:
  issuer: go-server-starter
  tokenSecret: your-secret-key
  tokenExpires:
    web: 24h
    mobile: 360h
    desktop: 360h

database:
  host: localhost
  port: 3306
  username: root
  password: your-password
  databaseName: your-db

redis:
  host: localhost
  port: 6379
  password: your-password
  db: 0

πŸ” Authentication & Authorization

JWT Authentication

The project uses JWT for stateless authentication with device-specific token expiration:

  • Web: 24 hours
  • Mobile/Desktop: 15 days
  • Chrome Extension: 30 days
  • API: 48 hours

Token Auto-Refresh

When a token's remaining lifetime drops below 1/3 of its total TTL, the server issues a new token in the new-token response header. The client should capture it and replace the old token:

// axios
axios.interceptors.response.use(res => {
  const newToken = res.headers['new-token']
  if (newToken) localStorage.setItem('token', newToken)
  return res
})

// fetch
const res = await fetch('/api/user/my-info', { headers })
const newToken = res.headers.get('new-token')
if (newToken) localStorage.setItem('token', newToken)

The old token remains valid until expiry β€” no requests are rejected during the overlap window.

Role-Based Access Control

Built-in roles:

  • super_admin - Super administrator
  • admin - Administrator
  • user - Regular user
  • user_vip - VIP user
  • user_svip - SVIP user
  • guest - Guest user

The six built-in roles have immutable metadata and cannot be deleted. super_admin permissions are also immutable; only super_admin can configure permissions on the other five built-in roles. Tenant administrators can create custom roles and assign permissions they currently hold, except platform-level tenant.* permissions. Tenant management APIs require the built-in super_admin role. User-role assignments are scoped by tenant, so the same user can have different roles in different tenants.

Protect routes with permission checks:

router.GET("/users", auth.PermissionCheckAny(constant.PermissionUserRead), handler)
router.DELETE("/users/:id", auth.PermissionCheckAny(constant.PermissionUserDelete), handler)

Login and tenant-switch responses include currentTenantId, active tenants, roles, and permissions. Clients can refresh access and tenant membership independently:

GET /api/auth/my-access

GET /api/auth/my-tenants

🌐 API Endpoints

Method Endpoint Description Auth
GET /api/hello Health check No
GET /api/healthz Liveness + readiness probe No
POST /api/auth/send-sms-code Send SMS verification code No
POST /api/auth/send-email-code Send email verification code No
POST /api/auth/login/mobile Login via mobile + code No
POST /api/auth/login/email Login via email + code No
POST /api/auth/switch-tenant Switch to another tenant Yes
GET /api/auth/my-tenants List current user's active tenants Yes
GET /api/user/my-info Get current user info Yes
PUT /api/user/my-info Update current user info Yes
GET /api/user/admin/table List users (paginated) admin+
GET /api/user/admin/{id} Get user by ID admin+
POST /api/user/admin Create user admin+
PUT /api/user/admin/{id} Update user admin+
DELETE /api/user/admin/{id} Delete user (soft) admin+
GET /api/role/{id} Get role by ID admin+
GET /api/role/table List roles (paginated) admin+
GET /api/role/{id}/permissions Get global permissions and role switch state role.read
POST /api/role Create role admin+
PUT /api/role/{id} Update role admin+
PUT /api/role/{id}/permissions Replace custom role permissions role.assign_permissions
PATCH /api/role/{id}/permissions/{permissionId} Toggle one custom role permission role.assign_permissions
DELETE /api/role/{id} Delete role (soft) admin+
GET /api/admin/tenants/code Generate tenant code super_admin
GET /api/admin/tenants/{id} Get tenant by ID super_admin
GET /api/admin/tenants List tenants (paginated) super_admin
POST /api/admin/tenants Create tenant super_admin
PUT /api/admin/tenants/{id} Update tenant super_admin
DELETE /api/admin/tenants/{id} Delete tenant (soft) super_admin
GET /api/admin/dead-letters List dead letters (DB) super_admin
POST /api/admin/dead-letters/retry Retry one dead letter super_admin
POST /api/admin/dead-letters/retry-all Retry all by type super_admin
DELETE /api/admin/dead-letters Delete dead letter super_admin

Swagger UI

Start the server and open http://localhost:8080/api/swagger/index.html for interactive API documentation. Use the Authorize button to set Bearer {token} for authenticated endpoints.

To regenerate docs after changing API annotations:

swag init -g cmd/server/server.go -o docs --parseDependency --parseInternal

🌍 Internationalization

The API supports multiple languages via the Accept-Language header:

# English
curl -H "Accept-Language: en" http://localhost:8080/api/hello

# Chinese
curl -H "Accept-Language: zh" http://localhost:8080/api/hello

🚦 Rate Limiting

Redis-backed GCRA algorithm with local fallback when Redis is unavailable.

Two modes

Mode Key Use
RateLimit IP address Unauthenticated endpoints (login, send-code)
RateLimitByUser User ID from JWT Authenticated endpoints

Per-route thresholds

Group Limit Key type
/auth/* (login, send-code) 10/min IP
/user/my-info 60/min User
/user/admin/* 120/min User
/role/* 120/min User
/admin/dead-letters/* 60/min User
/admin/tenants/* 60/min User
// Apply in route setup
router.Use(r.ratelimit.RateLimit(10, "AUTH"))        // IP-based
router.Use(r.ratelimit.RateLimitByUser(60, "USER"))  // User-based

Rate limit headers (X-RateLimit-*-Remaining, Retry-After) are included in responses.

πŸ“ Logging

Logs are structured with Zap and automatically rotated:

  • Log levels: debug, info, warn, error, fatal
  • Output: Console (dev) + File (info.log, error.log)
  • Rotation: Configurable max size, age, and backup count

πŸ—„οΈ Database Migration

Database migrations are managed by goose. All migration files are embedded in the binary and run automatically on startup β€” no external CLI required in production.

Migration format

Each migration is a single .sql file with -- +goose Up / -- +goose Down markers:

internal/database/migration/migrations/
  β”œβ”€β”€ 00001_create_user_roles.sql
  β”œβ”€β”€ 00002_create_tenants.sql
  β”œβ”€β”€ 00003_create_users.sql
  β”œβ”€β”€ 00004_create_user_tenant_refs.sql
  β”œβ”€β”€ 00005_create_user_tenant_role_refs.sql
  β”œβ”€β”€ 00006_create_permissions.sql
  β”œβ”€β”€ 00007_create_role_permission_refs.sql
  └── 00008_create_dead_letters.sql

Adding a new table

goose -dir internal/database/migration/migrations create add_new_table sql

Write DDL in the generated file, restart β€” migrations run on startup.

Adding indexes or constraints

Create a new migration with ALTER TABLE:

-- +goose Up
ALTER TABLE users ADD INDEX idx_status (status);
-- +goose Down
ALTER TABLE users DROP INDEX idx_status;

GORM struct tags (gorm:"index", gorm:"uniqueIndex") serve as documentation after goose takes over DDL. Indexes and unique constraints are defined exclusively in migration SQL.

Rollback

migration.Down(sqlDB)  // roll back the last migration

goose_db_version table tracks applied versions β€” never run twice, full rollback support.

πŸ“± Verification Codes & Notifications

Verification codes are stored in Redis with a 5-min TTL and a 60-second resend cooldown. Sending is handled asynchronously via the task queue.

Flow

POST /api/auth/send-sms-code  β†’  generate 6-digit code  β†’  Redis SETEX  β†’  taskq.EnqueueUnique  β†’  Alibaba Cloud SMS
POST /api/auth/login/mobile   β†’  Redis GET compare     β†’  DEL on match β†’  JWT token

Alibaba Cloud integration

pkg/notify/ provides pluggable SmsSender and EmailSender interfaces. The Alibaba Cloud implementations (AlibabaSmsSender, AlibabaEmailSender) are wired in app.go. To swap providers, implement the interface and change the wiring.

For local development, LogSender prints to console β€” no Alibaba Cloud credentials needed.

Email templates

Templates are HTML files embedded in the binary via embed.FS. The welcome email is at pkg/notify/template/templates/welcome_email.html. Add new templates as .html files in that directory β€” they are picked up automatically.

To render a template programmatically:

import notifytmpl "go-server-starter/pkg/notify/template"

html, _ := notifytmpl.GetEngine().Render("welcome_email.html", data)

πŸ“¨ Task Queue (Async Jobs)

Asynq provides Redis-backed task processing. Workers start alongside the HTTP server, and tasks are enqueued via pkg/taskq.

Adding a new task

  1. Define a task type constant and payload struct in pkg/taskq/tasks.go
  2. Create a constructor function (NewXxxTask)
  3. Write a handler function (HandleXxx)
  4. Register it in app.go: taskqServer.HandleFunc(taskq.TaskXxx, taskq.HandleXxx)
  5. Enqueue from your service:
task, _ := taskq.NewEmailWelcomeTask(taskq.EmailWelcomePayload{
    UserUniCode: user.UniCode,
    Email:       user.Email,
})
uniqueKey := taskq.WelcomeEmailUniqueKey(user.UniCode)
s.taskq.EnqueueUnique(ctx, task, uniqueKey, 24*time.Hour,
    taskq.RetryByType(taskq.TaskEmailWelcome)...)

Retry, idempotency & alerting

  • Per-task retry: RetryByType(taskType) returns MaxRetry + Timeout tuned per task.
  • Idempotency: EnqueueUnique uses asynq.Unique (Redis SETNX) to deduplicate tasks with the same key within a TTL window. Safe to call multiple times.
  • Exhausted retries: ErrorHandler logs the failure and calls the Alerter interface.
  • Alerter: pluggable β€” implement the Alerter interface to send Slack, webhook, or write to a dead-letter table. Pass your implementation to taskq.NewServer(..., alerter). Default is no-op.

Monitoring & dead-letter

Dashboard: install asynqmon to inspect Redis queues in real time:

go install github.com/hibiken/asynq/tools/asynqmon@latest
asynqmon --redis-addr=localhost:6379 --redis-password=root --redis-db=1
# open http://localhost:8081

Dead-letter persistence: retry-exhausted tasks are automatically written to MySQL (dead_letters table) via the Alerter. List, retry, or delete via admin API:

GET  /api/admin/dead-letters?taskType=email:welcome
POST /api/admin/dead-letters/retry        {"id": 5}
POST /api/admin/dead-letters/retry-all?taskType=email:welcome

Graceful shutdown

The task worker waits for in-flight tasks to complete before the process exits:

a.taskqServer.Shutdown()  // drain then stop
a.taskqClient.Close()     // close Redis connection

Tasks are archived by Asynq after retries are exhausted and can be inspected via asynqmon.

⏰ Cron Scheduler

robfig/cron schedules recurring jobs in-process. All jobs are registered in pkg/cronjob/register.go via cronjob.Register(repo, logger). app.go only calls Start()/Stop().

Adding a job

Add a Job entry in pkg/cronjob/register.go:

jobs := []Job{
    {Name: "my-job", Spec: "@every 1h", Fn: myJob(log)},
    // ...
}

Then implement myJob in the same file. No changes to app.go.

Supports 6-field cron (sec min hour dom month dow) and interval format (@every 1h30m).

Built-in jobs

Job Schedule Action
heartbeat @every 1h Log a heartbeat
purge-dead-letters 0 0 3 * * * (3am daily) Hard-delete retried dead letters older than 30 days

Graceful shutdown

a.cronSched.Stop()  // waits for running jobs to finish

πŸ› οΈ Development

Scaffold a Module

Generate a full CRUD module (model + repo + dto + service + handler + route + migration) in one command:

./generate.sh product
# or via Makefile
make generate MODULE=product

Output:

internal/model/product.go                      # data struct
internal/repo/product_repo.go                  # BaseRepo wrapper
internal/dto/product_dto.go                    # request/response types
internal/service/product_service.go            # business logic
internal/handler/product_handler.go            # HTTP binding
internal/router/product_router.go              # routes + permissions
internal/database/migration/migrations/xxx.sql # goose migration

The script also prints the exact lines you need to paste into repo.go, service.go, handler.go, and router.go to register the new module (3 files, ~4 lines each).

What gets touched

Adding a full CRUD module touches 12 files:

# File Purpose Auto?
1 internal/model/xxx.go Data struct βœ… generated
2 internal/database/migration/migrations/xxx.sql DDL βœ… generated
3 internal/repo/xxx_repo.go BaseRepo wrapper (10 lines) βœ… generated
4 internal/repo/repo.go Register repo to aggregate ✏️ 1 line each
5 internal/dto/xxx_dto.go Request/response types βœ… generated
6 internal/service/xxx_service.go Business logic (~100 lines) βœ… generated
7 internal/service/service.go Register service to aggregate ✏️ 1 line each
8 internal/handler/xxx_handler.go HTTP binding (~80 lines) βœ… generated
9 internal/handler/handler.go Register handler to aggregate ✏️ 1 line each
10 internal/router/xxx_router.go Routes + permissions (10 lines) βœ… generated
11 internal/router/router.go Register routes ✏️ 1 line
12 (none) goose auto-runs on restart βœ… automatic

Files marked βœ… are fully scaffolded by generate.sh. Files marked ✏️ need one insertion each β€” the script prints the exact code to paste.

To delete a module: ./generate.sh -d product.

Hot Reload

For development with hot reload, use Air:

air

Install Dev Tools

make install   # installs go-enum + swag CLI (one-time)
make enum      # regenerate enum code from // ENUM(...) comments
make swagger   # regenerate API docs from handler annotations

With Docker

  1. startup docker compose
docker compose -f docker-compose.dev.yml up -d
  1. startup Go app (hot reload, auto restart on code change)
air
  1. stop docker compose
docker compose -f docker-compose.dev.yml down

πŸ“„ License

This project is open-sourced software licensed under the MIT license.

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

A starter project in golang with tech stack: Gin + GORM + MySQL + Redis + JWT + Zap + koanf + goose + Asynq

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages