A production-ready Go web server boilerplate/starter kit with clean architecture, built-in authentication, RBAC, and comprehensive tooling.
- 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)
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
- Go 1.21+
- MySQL 8.0+
- Redis 6.0+
- Clone the repository:
git clone https://github.com/your-username/go-server-starter.git
cd go-server-starter- Install dependencies:
go mod download- Configure the application:
Copy and modify the config file for your environment:
cp configs/config.yml configs/config.dev.ymlEdit configs/config.dev.yml with your database and Redis settings.
- 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=testThe server will start on http://localhost:8080 by default.
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: 0The 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
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.
Built-in roles:
super_admin- Super administratoradmin- Administratoruser- Regular useruser_vip- VIP useruser_svip- SVIP userguest- 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
| 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 |
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 --parseInternalThe 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/helloRedis-backed GCRA algorithm with local fallback when Redis is unavailable.
| Mode | Key | Use |
|---|---|---|
RateLimit |
IP address | Unauthenticated endpoints (login, send-code) |
RateLimitByUser |
User ID from JWT | Authenticated endpoints |
| 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-basedRate limit headers (X-RateLimit-*-Remaining, Retry-After) are included in responses.
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 migrations are managed by goose. All migration files are embedded in the binary and run automatically on startup β no external CLI required in production.
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
goose -dir internal/database/migration/migrations create add_new_table sqlWrite DDL in the generated file, restart β migrations run on startup.
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.
migration.Down(sqlDB) // roll back the last migrationgoose_db_version table tracks applied versions β never run twice, full rollback support.
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.
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
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.
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)Asynq provides Redis-backed task processing. Workers start alongside the HTTP server, and tasks are enqueued via pkg/taskq.
- Define a task type constant and payload struct in
pkg/taskq/tasks.go - Create a constructor function (
NewXxxTask) - Write a handler function (
HandleXxx) - Register it in
app.go:taskqServer.HandleFunc(taskq.TaskXxx, taskq.HandleXxx) - 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)...)- Per-task retry:
RetryByType(taskType)returnsMaxRetry+Timeouttuned per task. - Idempotency:
EnqueueUniqueusesasynq.Unique(Redis SETNX) to deduplicate tasks with the same key within a TTL window. Safe to call multiple times. - Exhausted retries:
ErrorHandlerlogs the failure and calls theAlerterinterface. - Alerter: pluggable β implement the
Alerterinterface to send Slack, webhook, or write to a dead-letter table. Pass your implementation totaskq.NewServer(..., alerter). Default is no-op.
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:8081Dead-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
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 connectionTasks are archived by Asynq after retries are exhausted and can be inspected via asynqmon.
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().
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).
| 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 |
a.cronSched.Stop() // waits for running jobs to finishGenerate a full CRUD module (model + repo + dto + service + handler + route + migration) in one command:
./generate.sh product
# or via Makefile
make generate MODULE=productOutput:
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).
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.
For development with hot reload, use Air:
airmake install # installs go-enum + swag CLI (one-time)
make enum # regenerate enum code from // ENUM(...) comments
make swagger # regenerate API docs from handler annotations- startup docker compose
docker compose -f docker-compose.dev.yml up -d- startup Go app (hot reload, auto restart on code change)
air- stop docker compose
docker compose -f docker-compose.dev.yml downThis project is open-sourced software licensed under the MIT license.
Contributions are welcome! Please feel free to submit a Pull Request.