README
¶
Go Project Template
A production-ready Go project template following Clean Architecture and Domain-Driven Design principles, optimized for building scalable applications with PostgreSQL or MySQL.
Features
- Modular Domain Architecture - Domain-based code organization for scalability
- Clean Architecture - Separation of concerns with domain, repository, use case, and presentation layers
- Standardized Error Handling - Domain errors with proper HTTP status code mapping
- Dependency Injection Container - Centralized component wiring with lazy initialization and clean resource management
- Multiple Database Support - PostgreSQL and MySQL with dedicated repository implementations using
database/sql - Database Migrations - Separate migrations for PostgreSQL and MySQL using golang-migrate
- UUIDv7 Primary Keys - Time-ordered, sortable UUIDs for globally unique identifiers
- Transaction Management - TxManager interface for handling database transactions
- Transactional Outbox Pattern - Event-driven architecture with guaranteed delivery
- HTTP Server - Standard library HTTP server with middleware for logging and panic recovery
- Worker Process - Background worker for processing outbox events
- CLI Interface - urfave/cli for running server, migrations, and worker
- Health Checks - Kubernetes-compatible readiness and liveness endpoints
- Structured Logging - JSON logs using slog
- Configuration - Environment variable based configuration with go-env
- Input Validation - Advanced validation with jellydator/validation library including password strength, email format, and custom rules
- Password Hashing - Secure password hashing with Argon2id via go-pwdhash
- Docker Support - Multi-stage Dockerfile for minimal container size
- CI/CD - GitHub Actions workflow for linting and testing
- Comprehensive Makefile - Easy development and deployment commands
Project Structure
go-project-template/
├── cmd/
│ └── app/ # Application entry point
│ └── main.go
├── internal/
│ ├── app/ # Dependency injection container
│ │ ├── di.go
│ │ ├── di_test.go
│ │ └── README.md
│ ├── config/ # Configuration management
│ │ └── config.go
│ ├── database/ # Database connection and transaction management
│ │ ├── database.go
│ │ └── txmanager.go
│ ├── errors/ # Standardized domain errors
│ │ └── errors.go
│ ├── http/ # HTTP server and shared infrastructure
│ │ ├── middleware.go
│ │ ├── response.go
│ │ └── server.go
│ ├── httputil/ # HTTP utility functions
│ │ └── response.go # JSON responses and error mapping
│ ├── outbox/ # Outbox domain module
│ │ ├── domain/ # Outbox entities
│ │ │ └── outbox_event.go
│ │ └── repository/ # Outbox data access
│ │ ├── mysql_outbox_repository.go
│ │ └── postgresql_outbox_repository.go
│ ├── user/ # User domain module
│ │ ├── domain/ # User entities and domain errors
│ │ │ └── user.go
│ │ ├── http/ # User HTTP handlers
│ │ │ ├── dto/ # Request/response DTOs
│ │ │ │ ├── request.go
│ │ │ │ ├── response.go
│ │ │ │ └── mapper.go
│ │ │ └── user_handler.go
│ │ ├── repository/ # User data access
│ │ │ ├── mysql_user_repository.go
│ │ │ └── postgresql_user_repository.go
│ │ └── usecase/ # User business logic
│ │ └── user_usecase.go
│ ├── validation/ # Custom validation rules
│ │ ├── rules.go
│ │ └── rules_test.go
│ └── worker/ # Background workers
│ └── event_worker.go
├── migrations/
│ ├── mysql/ # MySQL migrations
│ └── postgresql/ # PostgreSQL migrations
├── .github/
│ └── workflows/
│ └── ci.yml
├── Dockerfile
├── Makefile
├── go.mod
└── go.sum
Domain Module Structure
The project follows a modular domain architecture where each business domain is organized in its own directory with clear separation of concerns:
domain/- Contains entities, value objects, domain types, and domain-specific errorsusecase/- Defines UseCase interfaces and implements business logic and orchestrationrepository/- Handles data persistence and retrieval, transforms infrastructure errors to domain errorshttp/- Contains HTTP handlers and DTOs (Data Transfer Objects)dto/- Request/response DTOs and mappers (API contracts)
Shared Utilities
app/- Dependency injection container for assembling application componentserrors/- Standardized domain errors for expressing business intenthttputil/- Shared HTTP utility functions including error mapping and JSON responsesconfig/- Application-wide configurationdatabase/- Database connection and transaction managementworker/- Background processing infrastructure
This structure makes it easy to add new domains (e.g., internal/product/, internal/order/) without affecting existing modules.
Prerequisites
- Go 1.25 or higher
- PostgreSQL 12+ or MySQL 8.0+
- Docker (optional)
- Make (optional, for convenience commands)
Quick Start
1. Clone the repository
git clone https://github.com/allisson/go-project-template.git
cd go-project-template
2. Customize the module path
After cloning, you need to update the import paths to match your project:
Option 1: Using find and sed (Linux/macOS)
# Replace with your actual module path
NEW_MODULE="github.com/yourname/yourproject"
# Update go.mod
sed -i "s|github.com/allisson/go-project-template|$NEW_MODULE|g" go.mod
# Update all Go files
find . -type f -name "*.go" -exec sed -i "s|github.com/allisson/go-project-template|$NEW_MODULE|g" {} +
Option 2: Using PowerShell (Windows)
# Replace with your actual module path
$NEW_MODULE = "github.com/yourname/yourproject"
# Update go.mod
(Get-Content go.mod) -replace 'github.com/allisson/go-project-template', $NEW_MODULE | Set-Content go.mod
# Update all Go files
Get-ChildItem -Recurse -Filter *.go | ForEach-Object {
(Get-Content $_.FullName) -replace 'github.com/allisson/go-project-template', $NEW_MODULE | Set-Content $_.FullName
}
Option 3: Manually
- Update the module name in
go.mod - Search and replace
github.com/allisson/go-project-templatewith your module path in all.gofiles
After updating, verify the changes and tidy dependencies:
go mod tidy
Important: Also update the .golangci.yml file to match your new module path:
formatters:
settings:
goimports:
local-prefixes:
- github.com/yourname/yourproject # Update this line
This ensures the linter correctly groups your local imports.
UUIDv7 Primary Keys
The project uses UUIDv7 for all primary keys instead of auto-incrementing integers. UUIDv7 provides several advantages:
Benefits:
- Time-ordered: UUIDs include timestamp information, maintaining temporal ordering
- Globally unique: No collision risk across distributed systems or databases
- Database friendly: Better index performance than random UUIDs (v4) due to sequential nature
- Scalability: No need for centralized ID generation or coordination
- Merge-friendly: Databases can be merged without ID conflicts
Implementation:
All ID fields use uuid.UUID type from github.com/google/uuid:
import "github.com/google/uuid"
type User struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name"`
Email string `db:"email"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
IDs are generated in the application code using uuid.NewV7():
user := &domain.User{
ID: uuid.Must(uuid.NewV7()),
Name: input.Name,
Email: input.Email,
Password: hashedPassword,
}
Database Storage:
- PostgreSQL:
UUIDtype (native support) - MySQL:
BINARY(16)type (16-byte storage)
Migration Example (PostgreSQL):
CREATE TABLE users (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Migration Example (MySQL):
CREATE TABLE users (
id BINARY(16) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. Install dependencies
go mod download
4. Configure environment variables
The application automatically loads environment variables from a .env file. Create a .env file in your project root (or any parent directory):
# Database configuration
DB_DRIVER=postgres # or mysql
DB_CONNECTION_STRING=postgres://user:password@localhost:5432/mydb?sslmode=disable
DB_MAX_OPEN_CONNECTIONS=25
DB_MAX_IDLE_CONNECTIONS=5
DB_CONN_MAX_LIFETIME=5
# Server configuration
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
# Logging
LOG_LEVEL=info
# Worker configuration
WORKER_INTERVAL=5
WORKER_BATCH_SIZE=10
WORKER_MAX_RETRIES=3
WORKER_RETRY_INTERVAL=1
Note: The application searches for the .env file recursively from the current working directory up to the root directory. This allows you to run the application from any subdirectory and it will still find your .env file.
Alternatively, you can export environment variables directly without a .env file.
5. Start a database (using Docker)
PostgreSQL:
make dev-postgres
MySQL:
make dev-mysql
6. Run database migrations
make run-migrate
7. Start the HTTP server
make run-server
The server will be available at http://localhost:8080
8. Start the worker (in another terminal)
make run-worker
Usage
HTTP Endpoints
Health Check
curl http://localhost:8080/health
Readiness Check
curl http://localhost:8080/ready
Register User
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "SecurePass123!"
}'
Password Requirements:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
Validation Errors:
If validation fails, you'll receive a 422 Unprocessable Entity response with details:
{
"error": "invalid_input",
"message": "email: must be a valid email address; password: password must contain at least one uppercase letter."
}
CLI Commands
The binary supports three commands via urfave/cli:
Start HTTP Server
./bin/app server
Run Database Migrations
./bin/app migrate
Run Event Worker
./bin/app worker
Error Handling
The project implements a standardized error handling system that expresses business intent rather than exposing infrastructure details.
Domain Error Architecture
Standard Domain Errors (internal/errors/errors.go)
The project defines standard domain errors that can be used across all modules:
var (
ErrNotFound = errors.New("not found") // 404 Not Found
ErrConflict = errors.New("conflict") // 409 Conflict
ErrInvalidInput = errors.New("invalid input") // 422 Unprocessable Entity
ErrUnauthorized = errors.New("unauthorized") // 401 Unauthorized
ErrForbidden = errors.New("forbidden") // 403 Forbidden
)
Domain-Specific Errors (internal/user/domain/user.go)
Each domain defines its own specific errors that wrap the standard errors:
var (
ErrUserNotFound = errors.Wrap(errors.ErrNotFound, "user not found")
ErrUserAlreadyExists = errors.Wrap(errors.ErrConflict, "user already exists")
ErrInvalidEmail = errors.Wrap(errors.ErrInvalidInput, "invalid email format")
)
Error Flow Through Layers
1. Repository Layer - Transforms infrastructure errors to domain errors:
func (r *PostgreSQLUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
querier := database.GetTx(ctx, r.db)
query := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1`
var user domain.User
err := querier.QueryRowContext(ctx, query, id).Scan(
&user.ID, &user.Name, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrUserNotFound // Infrastructure → Domain
}
return nil, apperrors.Wrap(err, "failed to get user by id")
}
return &user, nil
}
2. Use Case Layer - Returns domain errors directly:
func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) {
if strings.TrimSpace(input.Email) == "" {
return nil, domain.ErrEmailRequired // Domain error
}
if err := uc.userRepo.Create(ctx, user); err != nil {
return nil, err // Pass through domain errors
}
return user, nil
}
3. HTTP Handler Layer - Maps domain errors to HTTP responses:
func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
user, err := h.userUseCase.RegisterUser(r.Context(), input)
if err != nil {
httputil.HandleError(w, err, h.logger) // Auto-maps to HTTP status
return
}
httputil.MakeJSONResponse(w, http.StatusCreated, response)
}
HTTP Error Responses
The httputil.HandleError function automatically maps domain errors to appropriate HTTP status codes:
| Domain Error | HTTP Status | Error Code | Example Response |
|---|---|---|---|
ErrNotFound |
404 | not_found |
{"error":"not_found","message":"The requested resource was not found"} |
ErrConflict |
409 | conflict |
{"error":"conflict","message":"A conflict occurred with existing data"} |
ErrInvalidInput |
422 | invalid_input |
{"error":"invalid_input","message":"invalid email format"} |
ErrUnauthorized |
401 | unauthorized |
{"error":"unauthorized","message":"Authentication is required"} |
ErrForbidden |
403 | forbidden |
{"error":"forbidden","message":"You don't have permission"} |
| Unknown | 500 | internal_error |
{"error":"internal_error","message":"An internal error occurred"} |
Benefits
- No Infrastructure Leaks - Database errors are never exposed to API clients
- Business Intent - Errors express domain concepts (
ErrUserNotFoundvssql.ErrNoRows) - Consistent HTTP Mapping - Same domain error always maps to same HTTP status
- Type-Safe - Use
errors.Is()to check for specific error types - Structured Responses - All errors return consistent JSON format
- Centralized Logging - All errors are logged with full context before responding
Adding Errors to New Domains
When creating a new domain, define domain-specific errors:
// internal/product/domain/product.go
var (
ErrProductNotFound = errors.Wrap(errors.ErrNotFound, "product not found")
ErrInsufficientStock = errors.Wrap(errors.ErrConflict, "insufficient stock")
ErrInvalidPrice = errors.Wrap(errors.ErrInvalidInput, "invalid price")
)
Then use httputil.HandleError() in your HTTP handlers for automatic mapping.
Development
Build the application
make build
Run tests
make test
Run tests with coverage
make test-coverage
Run linter
make lint
Clean build artifacts
make clean
Docker
Build Docker image
make docker-build
Run server in Docker
make docker-run-server
Run worker in Docker
make docker-run-worker
Run migrations in Docker
make docker-run-migrate
Architecture
Database Repository Pattern
The project uses separate repository implementations for MySQL and PostgreSQL, leveraging Go's standard database/sql package. This approach provides:
- Database-specific optimizations - Each implementation is tailored to the specific database's features and syntax
- Type safety - Direct use of database/sql types without abstraction layers
- Clarity - Explicit SQL queries make it clear what operations are being performed
- No external dependencies - Uses only the standard library for database operations
Repository Interface (defined in use case layer):
// internal/user/usecase/user_usecase.go
type UserRepository interface {
Create(ctx context.Context, user *domain.User) error
GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error)
GetByEmail(ctx context.Context, email string) (*domain.User, error)
}
MySQL Implementation (internal/user/repository/mysql_user_repository.go):
func (r *MySQLUserRepository) Create(ctx context.Context, user *domain.User) error {
querier := database.GetTx(ctx, r.db)
query := `INSERT INTO users (id, name, email, password, created_at, updated_at)
VALUES (?, ?, ?, ?, NOW(), NOW())`
// Convert UUID to bytes for MySQL BINARY(16)
uuidBytes, err := user.ID.MarshalBinary()
if err != nil {
return apperrors.Wrap(err, "failed to marshal UUID")
}
_, err = querier.ExecContext(ctx, query, uuidBytes, user.Name, user.Email, user.Password)
if err != nil {
if isMySQLUniqueViolation(err) {
return domain.ErrUserAlreadyExists
}
return apperrors.Wrap(err, "failed to create user")
}
return nil
}
PostgreSQL Implementation (internal/user/repository/postgresql_user_repository.go):
func (r *PostgreSQLUserRepository) Create(ctx context.Context, user *domain.User) error {
querier := database.GetTx(ctx, r.db)
query := `INSERT INTO users (id, name, email, password, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())`
_, err := querier.ExecContext(ctx, query, user.ID, user.Name, user.Email, user.Password)
if err != nil {
if isPostgreSQLUniqueViolation(err) {
return domain.ErrUserAlreadyExists
}
return apperrors.Wrap(err, "failed to create user")
}
return nil
}
Key Differences:
| Feature | MySQL | PostgreSQL |
|---|---|---|
| UUID Storage | BINARY(16) - requires marshaling/unmarshaling |
Native UUID type |
| Placeholders | ? for all parameters |
$1, $2, $3... numbered parameters |
| Unique Errors | Check for "1062" or "duplicate entry" | Check for "duplicate key" or "unique constraint" |
Transaction Support:
The database.GetTx() helper function retrieves the transaction from context if available, otherwise returns the DB connection:
// internal/database/txmanager.go
type Querier interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
func GetTx(ctx context.Context, db *sql.DB) Querier {
if tx, ok := ctx.Value(txKey{}).(*sql.Tx); ok {
return tx
}
return db
}
This pattern ensures repositories work seamlessly within transactions managed by the use case layer.
Dependency Injection Container
The project uses a custom dependency injection (DI) container located in internal/app/ to manage all application components. This provides:
- Centralized component wiring - All dependencies are assembled in one place
- Lazy initialization - Components are only created when first accessed
- Singleton pattern - Each component is initialized once and reused
- Clean resource management - Unified shutdown for all resources
- Thread-safe - Safe for concurrent access across goroutines
Example usage:
// Create container with configuration
container := app.NewContainer(cfg)
// Get HTTP server (automatically initializes all dependencies)
server, err := container.HTTPServer()
if err != nil {
return fmt.Errorf("failed to initialize HTTP server: %w", err)
}
// Clean shutdown
defer container.Shutdown(ctx)
The container manages the entire dependency graph:
Container
├── Infrastructure (Database, Logger)
├── Repositories (User, Outbox)
├── Use Cases (User)
└── Presentation (HTTP Server, Worker)
For more details on the DI container, see internal/app/README.md.
Modular Domain Architecture
The project follows a modular domain-driven structure where each business domain is self-contained:
User Domain (internal/user/)
domain/- User entity and typesusecase/- UseCase interface and user business logic implementationrepository/- User data persistencehttp/- User HTTP endpoints and handlersdto/- Request/response DTOs and mappers
Outbox Domain (internal/outbox/)
domain/- OutboxEvent entity and status typesrepository/- Event persistence and retrieval
Shared Infrastructure
app/- Dependency injection container for component assemblyconfig/- Application configurationdatabase/- Database connection and transaction managementerrors/- Standardized domain errors (ErrNotFound, ErrConflict, etc.)http/- HTTP server, middleware, and shared utilitieshttputil/- Reusable HTTP utilities (JSON responses, error handling, status code mapping)worker/- Background event processing
Benefits of This Structure
- Scalability - Easy to add new domains without affecting existing code
- Encapsulation - Each domain is self-contained with clear boundaries
- Team Collaboration - Teams can work on different domains independently
- Maintainability - Related code is co-located, making it easier to understand and modify
- Dependency Management - Centralized DI container simplifies component wiring and testing
Adding New Domains
To add a new domain (e.g., product):
1. Create the domain structure
internal/product/
├── domain/
│ └── product.go # Domain entity + domain errors
├── usecase/
│ └── product_usecase.go # UseCase interface + business logic
├── repository/
│ ├── mysql_product_repository.go # MySQL data access
│ └── postgresql_product_repository.go # PostgreSQL data access
└── http/
├── dto/
│ ├── request.go # API request DTOs
│ ├── response.go # API response DTOs
│ └── mapper.go # DTO-to-domain mappers
└── product_handler.go # HTTP handlers (uses httputil.HandleError)
Define domain errors in your entity file:
// internal/product/domain/product.go
package domain
import (
"time"
apperrors "github.com/yourname/yourproject/internal/errors"
"github.com/google/uuid"
)
type Product struct {
ID uuid.UUID
Name string
Price float64
Stock int
CreatedAt time.Time
UpdatedAt time.Time
}
// Domain-specific errors
var (
ErrProductNotFound = apperrors.Wrap(apperrors.ErrNotFound, "product not found")
ErrInsufficientStock = apperrors.Wrap(apperrors.ErrConflict, "insufficient stock")
ErrInvalidPrice = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid price")
)
2. Register in DI container
Add the new domain to the dependency injection container (internal/app/di.go):
// Add fields to Container struct
type Container struct {
// ... existing fields
productRepo *productRepository.ProductRepository
productUseCase productUsecase.UseCase // Interface, not concrete type
productRepoInit sync.Once
productUseCaseInit sync.Once
}
// Add getter methods
func (c *Container) ProductRepository() (*productRepository.ProductRepository, error) {
var err error
c.productRepoInit.Do(func() {
c.productRepo, err = c.initProductRepository()
if err != nil {
c.initErrors["productRepo"] = err
}
})
// ... error handling
return c.productRepo, nil
}
func (c *Container) ProductUseCase() (productUsecase.UseCase, error) {
var err error
c.productUseCaseInit.Do(func() {
c.productUseCase, err = c.initProductUseCase()
if err != nil {
c.initErrors["productUseCase"] = err
}
})
// ... error handling
return c.productUseCase, nil
}
// Add initialization methods
func (c *Container) initProductRepository() (productUsecase.ProductRepository, error) {
db, err := c.DB()
if err != nil {
return nil, fmt.Errorf("failed to get database: %w", err)
}
// Select the appropriate repository based on the database driver
switch c.config.DBDriver {
case "mysql":
return productRepository.NewMySQLProductRepository(db), nil
case "postgres":
return productRepository.NewPostgreSQLProductRepository(db), nil
default:
return nil, fmt.Errorf("unsupported database driver: %s", c.config.DBDriver)
}
}
func (c *Container) initProductUseCase() (productUsecase.UseCase, error) {
txManager, err := c.TxManager()
if err != nil {
return nil, fmt.Errorf("failed to get tx manager: %w", err)
}
productRepo, err := c.ProductRepository()
if err != nil {
return nil, fmt.Errorf("failed to get product repository: %w", err)
}
return productUsecase.NewProductUseCase(txManager, productRepo)
}
3. Wire handlers in HTTP server
Update internal/http/server.go to register product routes:
productHandler := productHttp.NewProductHandler(container.ProductUseCase(), logger)
mux.HandleFunc("/api/products", productHandler.HandleProducts)
Tips:
- Define a UseCase interface in your usecase package to enable dependency inversion
- Define domain-specific errors in your domain package by wrapping standard errors
- Repository layer should transform infrastructure errors (like
sql.ErrNoRows) to domain errors - Use case layer should return domain errors directly without additional wrapping
- Use
httputil.HandleError()in HTTP handlers for automatic error-to-HTTP status mapping - Use the shared
httputil.MakeJSONResponsefunction for consistent JSON responses - Keep domain models free of JSON tags - use DTOs for API serialization
- Implement validation in your request DTOs
- Create mapper functions to convert between DTOs and domain models
- Register all components in the DI container for proper lifecycle management
- HTTP handlers should depend on the UseCase interface, not concrete implementations
Clean Architecture Layers
- Domain Layer - Contains business entities, domain errors, and rules (e.g.,
internal/user/domain) - Repository Layer - Data access implementations using
database/sql; transforms infrastructure errors to domain errors (e.g.,internal/user/repository) - Use Case Layer - UseCase interfaces and application business logic; returns domain errors (e.g.,
internal/user/usecase) - Presentation Layer - HTTP handlers that map domain errors to HTTP responses (e.g.,
internal/user/http) - Utility Layer - Shared utilities including error handling and mapping (e.g.,
internal/httputil,internal/errors)
Dependency Inversion Principle: The presentation layer (HTTP handlers) and infrastructure (DI container) depend on UseCase interfaces defined in the usecase layer, not on concrete implementations. This enables better testability and decoupling.
Error Flow: Errors flow from repository → use case → handler, where they are transformed from infrastructure concerns to domain concepts, and finally to appropriate HTTP responses.
Data Transfer Objects (DTOs)
The project enforces clear boundaries between internal domain models and external API contracts using DTOs:
Domain Models (internal/user/domain/user.go)
- Pure internal representation of business entities
- No JSON tags - completely decoupled from API serialization
- Focus on business rules and domain logic
DTOs (internal/user/http/dto/)
request.go- API request structures with validationresponse.go- API response structuresmapper.go- Conversion functions between DTOs and domain models
Example:
// Request DTO
type RegisterUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
func (r *RegisterUserRequest) Validate() error {
if r.Name == "" {
return errors.New("name is required")
}
// ... validation logic
}
// Response DTO
type UserResponse struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Mapper functions
func ToRegisterUserInput(req RegisterUserRequest) usecase.RegisterUserInput {
return usecase.RegisterUserInput{
Name: req.Name,
Email: req.Email,
Password: req.Password,
}
}
func ToUserResponse(user *domain.User) UserResponse {
return UserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
Benefits:
- Separation of Concerns - Domain models evolve independently from API contracts
- Security - Sensitive fields (like passwords) are never exposed in API responses
- Flexibility - Different API views of the same domain model (e.g., summary vs detailed)
- Versioning - Easy to maintain multiple API versions with different DTOs
- Validation - Request validation happens at the DTO level before reaching domain logic
Input Validation
The project uses the jellydator/validation library for comprehensive input validation at both the DTO and use case layers.
Custom Validation Rules (internal/validation/rules.go)
The project provides reusable validation rules:
// Password strength validation
PasswordStrength{
MinLength: 8,
RequireUpper: true,
RequireLower: true,
RequireNumber: true,
RequireSpecial: true,
}
// Email format validation
Email
// No leading/trailing whitespace
NoWhitespace
// Not blank after trimming
NotBlank
DTO Validation Example:
func (r *RegisterUserRequest) Validate() error {
err := validation.ValidateStruct(r,
validation.Field(&r.Name,
validation.Required.Error("name is required"),
appValidation.NotBlank,
validation.Length(1, 255).Error("name must be between 1 and 255 characters"),
),
validation.Field(&r.Email,
validation.Required.Error("email is required"),
appValidation.NotBlank,
appValidation.Email,
validation.Length(5, 255).Error("email must be between 5 and 255 characters"),
),
validation.Field(&r.Password,
validation.Required.Error("password is required"),
validation.Length(8, 128).Error("password must be between 8 and 128 characters"),
appValidation.PasswordStrength{
MinLength: 8,
RequireUpper: true,
RequireLower: true,
RequireNumber: true,
RequireSpecial: true,
},
),
)
return appValidation.WrapValidationError(err)
}
Validation Layers:
- DTO Layer - Validates API request structure and basic constraints
- Use Case Layer - Validates business logic rules and constraints
- Domain Layer - Defines domain-specific error types
Error Responses:
Validation errors are automatically wrapped as ErrInvalidInput and return 422 Unprocessable Entity:
{
"error": "invalid_input",
"message": "password: password must contain at least one uppercase letter."
}
Benefits:
- Declarative - Validation rules are clear and concise
- Reusable - Custom rules can be shared across the application
- Type-Safe - Compile-time validation of struct fields
- Extensible - Easy to add custom validation rules
- Consistent - Same validation logic at DTO and use case layers
- User-Friendly - Detailed error messages help API clients fix issues
Transaction Management
The template implements a TxManager interface for handling database transactions:
type TxManager interface {
WithTx(ctx context.Context, fn func(ctx context.Context) error) error
}
Transactions are automatically injected into the context and used by repositories.
HTTP Utilities
The httputil package provides shared HTTP utilities used across all domain modules:
MakeJSONResponse - Standardized JSON response formatting:
import "github.com/allisson/go-project-template/internal/httputil"
func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
product, err := h.productUseCase.GetProduct(r.Context(), productID)
if err != nil {
httputil.HandleError(w, err, h.logger)
return
}
httputil.MakeJSONResponse(w, http.StatusOK, product)
}
HandleError - Automatic domain error to HTTP status code mapping:
func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
user, err := h.userUseCase.RegisterUser(r.Context(), input)
if err != nil {
// Automatically maps domain errors to appropriate HTTP status codes
// ErrNotFound → 404, ErrConflict → 409, ErrInvalidInput → 422, etc.
httputil.HandleError(w, err, h.logger)
return
}
httputil.MakeJSONResponse(w, http.StatusCreated, response)
}
HandleValidationError - For JSON decode and validation errors:
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httputil.HandleValidationError(w, err, h.logger) // Returns 400 Bad Request
return
}
These utilities ensure consistent response formatting and error handling across all HTTP endpoints.
Transactional Outbox Pattern
User registration demonstrates the transactional outbox pattern:
- User is created in the database
user.createdevent is stored in the outbox table (same transaction)- Worker picks up pending events and processes them
- Events are marked as processed or failed
This guarantees that events are never lost and provides at-least-once delivery.
Configuration
All configuration is done via environment variables. The application automatically loads a .env file if present (searching recursively from the current directory up to the root).
Environment Variables
| Variable | Description | Default |
|---|---|---|
SERVER_HOST |
HTTP server host | 0.0.0.0 |
SERVER_PORT |
HTTP server port | 8080 |
DB_DRIVER |
Database driver (postgres/mysql) | postgres |
DB_CONNECTION_STRING |
Database connection string | postgres://user:password@localhost:5432/mydb?sslmode=disable |
DB_MAX_OPEN_CONNECTIONS |
Max open connections | 25 |
DB_MAX_IDLE_CONNECTIONS |
Max idle connections | 5 |
DB_CONN_MAX_LIFETIME |
Connection max lifetime | 5 |
LOG_LEVEL |
Log level (debug/info/warn/error) | info |
WORKER_INTERVAL |
Worker poll interval | 5 |
WORKER_BATCH_SIZE |
Events to process per batch | 10 |
WORKER_MAX_RETRIES |
Max retry attempts | 3 |
WORKER_RETRY_INTERVAL |
Retry interval | 1 |
Database Migrations
Migrations are located in migrations/postgresql and migrations/mysql directories.
Creating new migrations
- Create new
.up.sqland.down.sqlfiles with sequential numbering - Follow the naming convention:
000003_description.up.sql
Running migrations manually
Use the golang-migrate CLI:
migrate -path migrations/postgresql -database "postgres://user:password@localhost:5432/mydb?sslmode=disable" up
Testing
The project includes a CI workflow that runs tests with PostgreSQL.
Running tests locally
go test -v -race ./...
With coverage
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Dependencies
Core Libraries
- go-env - Environment variable configuration
- godotenv - Loads environment variables from .env files
- go-pwdhash - Password hashing with Argon2id
- validation - Advanced input validation library
- uuid - UUID generation including UUIDv7 support
- urfave/cli - CLI framework
- golang-migrate - Database migrations
Database Drivers
- lib/pq - PostgreSQL driver
- go-sql-driver/mysql - MySQL driver
License
MIT License - see LICENSE file for details
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Acknowledgments
This template uses the following excellent Go libraries:
- github.com/allisson/go-env
- github.com/allisson/go-pwdhash
- github.com/jellydator/validation
- github.com/urfave/cli
- github.com/golang-migrate/migrate
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
app
command
Package main provides the entry point for the application with CLI commands.
|
Package main provides the entry point for the application with CLI commands. |
|
internal
|
|
|
app
Package app provides dependency injection container for assembling application components.
|
Package app provides dependency injection container for assembling application components. |
|
config
Package config provides application configuration management through environment variables.
|
Package config provides application configuration management through environment variables. |
|
database
Package database provides database connection management and configuration.
|
Package database provides database connection management and configuration. |
|
errors
Package errors provides standardized domain errors that express business intent rather than infrastructure details.
|
Package errors provides standardized domain errors that express business intent rather than infrastructure details. |
|
http
Package http provides HTTP server implementation and request handlers.
|
Package http provides HTTP server implementation and request handlers. |
|
httputil
Package httputil provides HTTP utility functions for request and response handling.
|
Package httputil provides HTTP utility functions for request and response handling. |
|
outbox/domain
Package domain defines the core outbox domain entities and types.
|
Package domain defines the core outbox domain entities and types. |
|
outbox/repository
Package repository provides data persistence implementations for outbox entities.
|
Package repository provides data persistence implementations for outbox entities. |
|
user/domain
Package domain defines the core user domain entities and types.
|
Package domain defines the core user domain entities and types. |
|
user/http
Package http provides HTTP handlers for user-related operations.
|
Package http provides HTTP handlers for user-related operations. |
|
user/http/dto
Package dto provides data transfer objects for the user HTTP layer.
|
Package dto provides data transfer objects for the user HTTP layer. |
|
user/repository
Package repository provides data persistence implementations for user entities.
|
Package repository provides data persistence implementations for user entities. |
|
user/usecase
Package usecase implements the user business logic and orchestrates user domain operations.
|
Package usecase implements the user business logic and orchestrates user domain operations. |
|
validation
Package validation provides custom validation rules for the application.
|
Package validation provides custom validation rules for the application. |
|
worker
Package worker provides background workers for processing asynchronous tasks.
|
Package worker provides background workers for processing asynchronous tasks. |