A production-ready Go backend template built on clean architecture principles, designed for rapid development of scalable REST API services with enterprise-grade features.
ichi-go accelerates backend development through proven architectural patterns, comprehensive tooling, and developer-friendly abstractions. Built for teams with varying skill levels, it reduces cognitive load while maintaining flexibility for complex business requirements.
- Clean Architecture: Domain-driven design with clear separation of concerns
- Production Ready: JWT auth, validation, logging, error handling out of the box
- Developer Experience: CLI generators, hot reload, comprehensive examples
- Multi-driver Queue: RabbitMQ (AMQP) and PostgreSQL/River backends β switchable via config
- Multi-driver Database: Named connection map supports MySQL and PostgreSQL via Bun ORM
- Migrations: Goose with auto-detected driver (MySQL or Postgres dialect)
- Caching: Redis with LZ4 compression
- Validation: Multilingual support (English/Indonesian) with custom validators
- Observability: Structured logging with zerolog, request tracing
- Tech Stack
- Getting Started
- Project Structure
- Core Features
- Development Workflow
- Database Management
- Code Generation
- Configuration
- Testing
- Examples
- Best Practices
- Go 1.24+ - Primary language
- Echo v4 - HTTP framework
- Bun ORM - Database operations (MySQL + PostgreSQL dialects)
- Viper - Type-safe configuration management
- Redis - Caching with LZ4 compression
- RabbitMQ (optional) - AMQP message queue with topic exchanges
- MySQL - Primary relational database
- PostgreSQL (optional) - Alternate database / River queue backend
- riverqueue/river (optional) - DB-backed reliable job queue on Postgres
- Air - Hot reload for development
- Goose - SQL migrations (auto-detects MySQL / Postgres dialect)
- samber/do - Runtime dependency injection
- Mockery - Interface mocking for tests
- JWT - Multiple algorithm support (HMAC, RSA, ECDSA)
- go-playground/validator - Request validation with i18n
- zerolog - Structured logging
- Request ID - Distributed tracing support
The fastest way to get started is using Docker:
# 1. Update production config
nano config.production.yaml
# Change JWT secret and sensitive values
# 2. Deploy everything
make docker-deploy
# 3. Access
# API: http://localhost:8080
# API Docs: http://localhost:8080/docs/index.htmlπ See README.Docker.md for complete Docker deployment guide
# Required
go >= 1.24
redis >= 7.0
# At least one of:
mysql >= 8.0 # default primary DB
postgres >= 15 # if using Postgres DB or River queue backend
# Optional
rabbitmq >= 3.12 # if queue.connections.amqp.enabled = true
# Development tools
make
air # for hot reload- Clone and setup
git clone <repository-url>
cd ichi-go
cp config.example.yaml config.local.yaml- Configure environment
# config.local.yaml
database:
default: "mysql" # or "postgres"
connections:
mysql:
host: "localhost"
port: 3306
user: "your_user"
password: "your_password"
name: "your_database"
# postgres: # uncomment for Postgres / River queue
# host: "localhost"
# port: 5432
# user: "postgres"
# name: "your_database"
cache:
host: "localhost"
port: 6379
queue:
default: "amqp" # or "database" for River/Postgres
connections:
amqp:
enabled: true # set true if RabbitMQ is available
database:
enabled: false # set true to use River on Postgres- Install dependencies
go mod download
go install github.com/pressly/goose/v3/cmd/goose@latest
go install github.com/vektra/mockery/v2@latest
go install github.com/air-verse/air@latest- Setup database
make migration-up # Run migrations
make seed-run # Seed initial data- Run the application
# Development with hot reload
air
# Or direct execution
go run cmd/main.goThe server will start on http://localhost:8080
ichi-go/
βββ cmd/
β βββ main.go # Application entry point
β βββ server/
β β βββ rest_server.go # HTTP routes setup
β β βββ queue_server.go # Queue workers (AMQP + River)
β β βββ web_server.go # Web/template routes
β βββ validator/
β βββ setup.go # Validator initialization
βββ internal/
β βββ applications/ # Domain applications
β β βββ auth/ # Authentication (login, register, tokens)
β β βββ user/ # User management (reference CRUD example)
β β βββ health/ # Health check endpoints
β β βββ notification/ # Notification service (blast + user-specific)
β β βββ rbac/ # Role-based access control (36 endpoints)
β βββ infra/ # Infrastructure layer
β β βββ database/ # Bun ORM (MySQL + Postgres)
β β βββ cache/ # Redis client
β β βββ queue/ # Driver-agnostic queue layer
β β β βββ interfaces.go # Dispatcher, JobArgs, ConsumeFunc
β β β βββ options.go # DispatchOption helpers
β β β βββ dispatcher.go # NewDispatcher factory (amqp | database)
β β β βββ rabbitmq/ # AMQP producer/consumer
β β β βββ river/ # River worker pool (Postgres-backed)
β β βββ authz/ # Casbin RBAC enforcer, adapter, cache, watcher
β βββ middlewares/ # HTTP middlewares
βββ db/
β βββ cmd/
β β βββ migrate.go # Migration CLI (auto-detects MySQL/Postgres)
β βββ migrations/
β βββ schema/ # Schema migrations
β βββ data/ # Data migrations
β βββ seeds/ # Seed files
βββ pkg/ # Reusable packages
β βββ authenticator/ # JWT implementation
β βββ validator/ # Validation framework
β βββ logger/ # Structured logging
β βββ requestctx/ # Request context
β βββ utils/
β βββ response/ # HTTP response builders
β βββ dto/ # DTO mapping
βββ config/ # Configuration schemas
β βββ config.go # Main config loader
β βββ app/
β βββ http/
βββ Makefile # Build automation
Multi-algorithm support with automatic key management:
# config.local.yaml
auth:
jwt:
enabled: true
signing_method: "HS256" # or RS256, ES256
secret_key: "your-secret"
access_token_ttl: "1h"
refresh_token_ttl: "720h"
skip_paths:
- "/api/v1/auth/*"
- "/api/v1/public/*"Supported Algorithms:
- HS256/HS384/HS512: HMAC with secret key
- RS256/RS384/RS512: RSA with public/private keys
- ES256/ES384/ES512: ECDSA with public/private keys
Domain-isolated validation with multilingual support:
// Define custom validator
type EmailValidator struct {
Tag: "email_domain",
Fn: func(fl validator.FieldLevel) bool {
// Custom validation logic
},
RegisterTrans: func(trans ut.Translator) error {
return trans.Add("email_domain",
"Email must be from allowed domain", true)
},
}
// Register in domain
validators.RegisterAuthValidators(validator)
// Use in handler
if err := validator.BindAndValidate(c, &req); err != nil {
return response.Error(c, http.StatusBadRequest, err)
}Language Detection:
X-Languageheader (priority)Accept-Languageheader- Falls back to configured default
Driver-agnostic queue.Dispatcher interface β configure AMQP (RabbitMQ) or database (River on Postgres) via queue.default:
// Register consumer (works for both drivers)
queue.RegisterConsumer(queue.ConsumerRegistration{
Name: "payment_handler",
Description: "Processes payment events",
ConsumeFunc: func(ctx context.Context, payload []byte) error {
// Process message
return nil
},
})
// Dispatch a job (driver-agnostic)
dispatcher.Dispatch(ctx, myJob,
queue.WithDelay(5*time.Minute), // schedule delay
)AMQP (RabbitMQ) backend:
- Topic-based routing with delayed message support
- Configurable worker pools per consumer
- Automatic reconnection handling
Database (River) backend:
- Postgres-backed reliable job queue
- Poll-based worker with configurable concurrency
- Supports
Queue,MaxAttempts,Priorityinsert options - Shares the existing Bun
*sql.DBconnection β no extra pool needed
Runtime dependency injection with automatic lifecycle management:
// Register dependencies
func Register(injector do.Injector, serviceName string, e *echo.Echo) {
// Repositories
do.ProvideNamed(injector, serviceName+".user.repository",
repositories.NewUserRepository)
// Services
do.ProvideNamed(injector, serviceName+".user.service",
services.NewUserService)
// Controllers
do.ProvideNamed(injector, serviceName+".user.controller",
controllers.NewUserController)
}
// Invoke dependencies
userService := do.MustInvokeNamed[*services.UserService](
injector,
serviceName+".user.service",
)Benefits:
- No code generation required
- Runtime flexibility
- Automatic shutdown handling via
injector.Shutdown() - Named dependencies for multi-tenancy support
Centralized request metadata:
type RequestContext struct {
RequestID string
UserID string
Language string
ClientIP string
ValidatedClaims any
}
// Extract from Echo context
rc := requestctx.FromRequest(c.Request())
// Use in business logic
userID := requestctx.GetUserID(ctx)Multi-tenant role-based access control with Casbin v3:
- Three-layer model: Platform β Application (tenant-scoped) β Resource (future ABAC)
- L1/L2 caching: in-memory LRU + Redis for <1ms permission checks
- Audit logging: SOC2/GDPR-compliant with 7-year retention
- Pre-seeded: 9 system roles, 146 default permissions
// Protect a route group β auto-maps HTTP methods to actions
admin := e.Group("/admin")
admin.Use(RequirePermission(enforcementService, "admin", "access"))
// Check permission in service code
allowed, err := enforcementService.CheckPermission(
ctx, userID, tenantID, "products", "edit")See docs/RBAC.md for complete documentation.
# Start with Air (auto-reload on file changes)
air
# Air watches:
# - Go files
# - Config files (*.yaml)
# - Templates (*.html)# Full CRUD with all components
make ichigen-example-full
# Or using the CLI directly
go run ./pkg/ichigen/cmd/main.go g full product --domain=catalog --crud
# Creates:
# - Controller (HTTP handlers)
# - Service (business logic)
# - Repository (data access)
# - DTO (request/response models)
# - Validators (domain-specific validation)
# - Dependency injection setup with samber/do# Generate controller only
go run ./pkg/ichigen/cmd/main.go g controller notification --domain=alert
# Or use Makefile examples
make ichigen-example-controller# Build and install
make ichigen-install
# Now use anywhere
ichigen g full order --domain=sales --crud# Generate all mocks
mockery --all --dir internal/applications --output mocks --keeptree
# Generate specific interface
mockery --name=UserRepository --dir internal/applications/user# Create migration
make migration-create name=create_products_table
# Run migrations
make migration-up
# Rollback last
make migration-down
# Status
make migration-status
# Migrate to specific version
make migration-up-to version=20250407085044
# Reset all (DANGEROUS!)
make migration-reset# Create data migration
make data-migration-create name=fix_legacy_emails
# Run data migrations
make data-migration-up
# Check status
make data-migration-status# Run all seeds
make seed-run
# Run specific seed
make seed-file name=00_base_roles.sql
# List available seeds
make seed-list# Reset + Migrate + Seed (development only)
make db-reset-dev
# Fresh setup (no reset)
make db-fresh
# Complete status
make db-statusconfig.local.yaml # Local development
config.dev.yaml # Development server
config.staging.yaml # Staging environment
config.prod.yaml # Production
Set environment:
export APP_ENV=prod
go run cmd/main.goapp:
env: "local"
name: "ichi-go"
debug: true
http:
port: 8080
timeout: 10000
cors:
allow_origins: ["*"]
# Named connection map β add as many drivers as needed
database:
default: "mysql" # which connection is the primary *bun.DB
connections:
mysql:
driver: "mysql"
host: "localhost"
port: 3306
name: "ichi_app"
max_idle_conns: 10
max_open_conns: 100
# postgres: # uncomment for Postgres / River
# driver: "postgres"
# host: "localhost"
# port: 5432
# name: "ichi_app"
# ssl_mode: "disable"
cache:
driver: "redis"
host: "localhost"
pool_size: 20
validator:
default_language: "en"
supported_languages: ["en", "id"]
# Multi-driver queue β enable one or both backends
queue:
default: "amqp" # active dispatcher; "database" for River
connections:
amqp:
enabled: true
driver: "amqp"
rabbitmq:
exchanges:
- name: "app.events"
type: "x-delayed-message"
consumers:
- name: "payment_handler"
queue:
name: "order.payment.events"
routing_keys:
- "payment.completed"
prefetch_count: 50
worker_pool_size: 10
database:
enabled: false
driver: "database"
database:
connection: "postgres" # must match a key in database.connections
max_workers: 50
poll_interval: 1s# Run all tests
go test ./...
# With coverage
go test -cover ./...
# Specific package
go test ./internal/applications/user/services/...
# Verbose output
go test -v ./...func TestUserService_Create(t *testing.T) {
mockRepo := new(mock_user.UserRepository)
service := services.NewUserService(mockRepo)
mockRepo.On("Create", mock.Anything, mock.Anything).
Return(&user.User{ID: 1}, nil)
result, err := service.Create(context.Background(), dto)
assert.NoError(t, err)
assert.Equal(t, 1, result.ID)
mockRepo.AssertExpectations(t)
}See /internal/applications/user for reference implementation:
user/
βββ controllers/
β βββ user_controller.go # HTTP handlers
βββ services/
β βββ user_service.go # Business logic
βββ repositories/
β βββ user_repository.go # Data access
βββ dto/
β βββ create_user_dto.go
β βββ update_user_dto.go
βββ validators/
β βββ user_validators.go # Custom validation
βββ models/
β βββ user.go # Domain model
βββ register.go # DI registration with samber/do
// 1. Login endpoint
func (c *AuthController) Login(ctx echo.Context) error {
var req dto.LoginRequest
if err := validator.BindAndValidate(ctx, &req); err != nil {
return response.Error(ctx, http.StatusBadRequest, err)
}
token, err := c.service.Login(ctx.Request().Context(), req)
if err != nil {
return response.Error(ctx, http.StatusUnauthorized, err)
}
return response.Success(ctx, token)
}
// 2. Protected endpoint
func (c *UserController) GetProfile(ctx echo.Context) error {
userID := requestctx.GetUserID(ctx.Request().Context())
profile, err := c.service.GetByID(ctx.Request().Context(), userID)
return response.Success(ctx, profile)
}// Register in internal/infra/queue/registry.go
func init() {
RegisterConsumer(ConsumerRegistration{
Name: "welcome_notifier",
Description: "Sends welcome emails to new users",
ConsumeFunc: handleWelcomeNotification,
})
}
func handleWelcomeNotification(ctx context.Context, msg rabbitmq.Message) error {
var userData UserCreatedEvent
if err := json.Unmarshal(msg.Body, &userData); err != nil {
return err
}
// Send email logic
logger.Infof("Sending welcome email to %s", userData.Email)
return nil
}// In register.go
func Register(injector do.Injector, serviceName string, e *echo.Echo, auth *authenticator.Authenticator) {
// Register repository with named dependency
do.ProvideNamed(injector, serviceName+".user.repository",
func(i do.Injector) (*repositories.UserRepository, error) {
db := do.MustInvoke[*database.DB](i)
return repositories.NewUserRepository(db), nil
})
// Register service with dependency on repository
do.ProvideNamed(injector, serviceName+".user.service",
func(i do.Injector) (*services.UserService, error) {
repo := do.MustInvokeNamed[*repositories.UserRepository](
i, serviceName+".user.repository")
return services.NewUserService(repo), nil
})
// Register controller and setup routes
do.ProvideNamed(injector, serviceName+".user.controller",
func(i do.Injector) (*controllers.UserController, error) {
service := do.MustInvokeNamed[*services.UserService](
i, serviceName+".user.service")
return controllers.NewUserController(service), nil
})
// Setup routes
setupRoutes(injector, serviceName, e, auth)
}// Use typed errors
var ErrUserNotFound = errors.New("user not found")
// Return domain errors
func (s *UserService) GetByID(ctx context.Context, id string) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}// Define DTOs with validation tags
type CreateUserDTO struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Name string `json:"name" validate:"required,min=2,max=100"`
}
// Use BindAndValidate in handlers
if err := validator.BindAndValidate(c, &dto); err != nil {
return response.Error(c, http.StatusBadRequest, err)
}// Use contextual logging
logger.Infof("Processing order %s", orderID)
logger.Debugf("User data: %+v", user)
logger.Errorf("Database error: %v", err)
// With request context
logger.WithContext(ctx).Infof("Request processed")// Always use named dependencies for domain isolation
serviceName := "myapp"
do.ProvideNamed(injector, serviceName+".user.repository", ...)
// Use MustInvoke for required dependencies
db := do.MustInvoke[*database.DB](injector)
// Proper shutdown in main.go
defer injector.Shutdown() // Cleans up all resources- Unit tests: Test services with mocked repositories
- Integration tests: Test repositories with test database
- E2E tests: Test complete request flow
// Always handle context cancellation
func handleMessage(ctx context.Context, msg rabbitmq.Message) error {
select {
case <-ctx.Done():
return ctx.Err() // Graceful shutdown
default:
// Process message
}
}
// Use proper error handling for retry logic
func handleMessage(ctx context.Context, msg rabbitmq.Message) error {
if err := process(msg); err != nil {
if isRetryable(err) {
return err // Will be requeued
}
logger.Errorf("Non-retryable error: %v", err)
return nil // Ack to prevent infinite retry
}
return nil
}- Starter Guide - New developer Day 1 guide
- Adding a New Service - Domain creation walkthrough
- RBAC Guide - Authorization system reference
- Testing Guide - Comprehensive testing patterns
- Testing Quick Start - Get testing in 5 minutes
- Echo Framework Docs
- Bun ORM Guide
- samber/do Documentation
- Go-Playground Validator
- RabbitMQ Tutorials
# Development
make ichigen-build # Build CLI generator
make ichigen-install # Install generator globally
make migration-help # Show all migration commands
make db-status # Complete database status
# Testing
go test ./... # Run all tests
go test -race ./... # Race condition detection
go test -bench=. # Run benchmarks
# Code Quality
go vet ./... # Static analysis
go fmt ./... # Format code
golangci-lint run # Comprehensive linting- Follow the existing code structure
- Write tests for new features
- Update documentation
- Use conventional commit messages
- Ensure all tests pass before submitting PR
MIT License
Copyright (c) 2025 ichi-go contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Built with β€οΈ for teams that ship fast without compromising quality