🎉 This is a template for creating a Go web service API project. Intended to be used as a starting point for creating a new Go web service API project and be a guideline for the project structure.
- Install
gonew
go install golang.org/x/tools/cmd/gonew@latest- Create a new project
gonew github.com/kongsakchai/gotemplate/template github.com/yourname/projectnameCommon
./
├── cache
├── database
├── errs
├── httpclient
├── logger
├── pkg
└── validator- cache Cache connectors, such as Redis.
- database Database connectors and setup, e.g., MySQL or PostgreSQL.
- errs Custom error types and centralized error handling for error tracking.
- httpclient HTTP client utilities for calling external services or APIs.
- logger Logging configuration and shared logger instances.
- pkg A collection of small helper packages used across the project.
- validator Request data validation logic, e.g., using go-playground/validator.
Template
./
├── .script
├── app
│ ├── apperror
│ └── middleware
├── config
├── docs
└── migrations- app Application layer and business logic.
- app/apperror Global error handler.
- app/middleware HTTP middleware for request processing, such as authentication, authorization, and logging.
- config Application configuration files and environment variable management.
- docs API documentation, e.g., using go-swagger.
- migrations Database migration files (.sql) for schema changes, e.g., using kongsakchai/simple-sql-migrate.
A helper package for interacting with caching systems. It includes utilities such as a Redis client factory. You may also integrate other caching solutions, such as github.com/patrickmn/go-cache.
A package for creating database connectors. Files should be organized by database type, for example:
- database/mysql.go
- database/postgres.go
- database/mongo.go
A helper package for error handling and error tracing:
newErr := errs.Wrap(/* normal error */ err)
// OR
newErr := errs.New("some error")
fmt.Println(newErr.Error())error: msg at (file.go:line) package.function
A helper package for interact with external API using HTTP client. Contain a function to call external API and return the ressult
type Response[T any] struct {
Code int // http code
Data T
RawData []byte // raw rasponse
}httpclient.Get[Resp any](ctx context.Context, client *Client, url string, headers ...http.Header) (Response[Resp], error)
httpclient.Post[Resp any](ctx context.Context, client *Client, url string, payload any, headers ...http.Header) (Response[Resp], error)
httpclient.Put[Resp any](ctx context.Context, client *Client, url string, payload any, headers ...http.Header) (Response[Resp], error)
httpclient.Delete[Resp any](ctx context.Context, client *Client, url string, payload any, headers ...http.Header) (Response[Resp], error)A helper package for configuring the application logger. You can control the log level, format, and enable/disable logging via environment variables.
LOG_ENABLE=true
LOG_HTTP_ENABLE=true
LOG_LEVEL=debug|info|warning|error|critical
LOG_FORMAT=text|jsonSensitive data masking (such as passwords, tokens, or PII) can be configured in logger/replace.go.
pkg/timer— A small package that defines aTimerinterface and a concrete implementation. Purpose: allow injecting the time source so code that depends on the current time can be tested deterministically.pkg/mockutil— Test helpers and mocks used in unit tests to replace real implementations with controllable test doubles.
A package for defining validation rules for requests or structs using validation tags, powered by go-playground/validator.
This is the main package we will focus on. Business logic and application layers should reside in this package, with each module clearly separated. Example:
./
└── app
├── user
└── admin
├── admin.go
└── handler.go- app/register
- app/booking
- app/product
Caution
Business logic should not be written in any package other than app/.
app/app.go provides helpers for API responses:
type Response struct {
Code string `json:"code"` // business code
Success string `json:"success"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
}OK 200
app.Ok(ctx echo.Context, data any, msg ...string) error
// usage
app.OK(ctx, "data","success")status: 200
body: { 'code': '0000', 'success': true, 'data': 'data', 'message': 'success' }Created 201
app.Created(ctx echo.Context, data any, msg ...string) error
// usage
app.Created(ctx, "data","success")status: 201
body: { 'code': '0000', 'success': true, 'data': 'data', 'message': 'success' }API Error Response — app/error.go
type Error struct {
HTTPCode int // HTTP status code: 500, 400, 401, 403, 409
Code string // Business code
Message string
Data any
Err error // Used for server-side logging only
}app.InternalServer(code string, msg string, err error, data ...any) app.Error
app.BadRequest(code string, msg string, err error, data ...any) app.Error
app.NotFound(code string, msg string, err error, data ...any) app.Error
app.Unauthorized(code string, msg string, err error, data ...any) app.Error
app.Forbidden(code string, msg string, err error, data ...any) app.Error
app.Conflict(code string, msg string, err error, data ...any) app.ErrorNote
- Why I do not use HTTP
404for data not found ?404is a standard HTTP error that represents a missing endpoint or resource. Using it for missing data can cause confusion between “data not found” and “route not found”, and it also adds unnecessary complexity on the client side. - Why I use HTTP
400for data not found ? I see missing data as something that usually results from an invalid or incorrect request from the client, while the system itself is still operating normally.
500 Internal Server Error
app.Fail(ctx echo.Context, err app.Error) error
// usage
app.Fail(ctx, app.InternalServer(app.ErrInternalCode, app.ErrInternalMsg, err))status: 500
body: { 'code': '9999', 'success': false, 'message': 'internal error' }Global Error Handler
apperror.ErrorHandler(err error, ctx echo.Context)
// Usage
echoApp.HTTPErrorHandler = apperror.ErrorHandler // already configured in route.goYou can return an app.Error directly from a handler:
func healthCheck(db *sqlx.DB) echo.HandlerFunc {
return func(ctx echo.Context) error {
if db.Ping() != nil {
return app.InternalServer(app.ErrInternalCode, app.ErrDatabaseMsg, nil)
}
return app.Ok(ctx, nil, "healthy")
}
}A helper package for HTTP middleware used in request processing, such as authentication, authorization, logging, and request tracing.
app/middleware/refid.go
Middleware for managing a reference ID to make log tracing easier. The header key can be configured via an environment variable.
HEADER_REF_ID_KEY=If the reference ID is not present in the request header, a new one will be generated using github.com/google/uuid.
app/middleware/logger.go
Middleware for logging API request and response data.
All configuration should be read and stored as structs within this package. You can differentiate environments using the ENV variable and per-environment prefixes:
ENV=LOCAL|DEV|PROD
LOCAL_DATABASE_URL=
DEV_DATABASE_URL=
PROD_DATABASE_URL=type Database struct {
URL string `env:"DATABASE_URL"`
}A folder containing SQL files for database migrations or schema updates. Migration files follow this naming convention:
version_name.up.sql
version_name.down.sql
Example:
0001_init_schema.up.sql
Migration behavior can be configured via environment variables:
MIGRATION_ENABLE=true
MIGRATION_DIR=./migrations
MIGRATION_VERSION=0001
MIGRATION_REPEAT=none- If
MIGRATION_VERSION,MIGRATION_VERSIONand,MIGRATION_REPEATis not specified, the latest version will be used MIGRATION_DIRshould not empty
When using this Go template, I recommend the following patterns:
- Storage pattern / Repository pattern for managing database or external API interactions to separate concerns and improve testability.
- Combine Handler with Service I don't see the necessity to separate Handler from Service, as it may overcomplicate the code, especially for small to medium projects. Combining them reduces file count and improves code clarity and maintainability. However, I recommend breaking down Handler into smaller functions for better organization:
Handlefunction: manages HTTP requestsProcessfunction: handles business logic (Service layer)
Example:
type handler struct {
storage Storager
}
func (h *handler) GetUserByID(ctx echo.Context) error {
userID := ctx.Param("id")
_, err := h.processGetUserByID(userID)
if err != nil {
return err
}
return nil
}
func (h *handler) processGetUserByID(userID string) (*User, error) {
// business logic here
}- One file per endpoint for clarity and easier maintenance. In larger projects, organizing files by endpoint improves code organization and makes features easier to locate and modify.
- Separate modules by business domain for better organization and maintainability. Domain-driven module separation improves code clarity and reduces cognitive load.
- File naming should represent the responsibility and purpose of the file.
- Error handling Use centralized error handling by creating custom error types and leveraging the global error handler to manage all errors in one place. This keeps code clean and simplifies maintenance.
This project uses testify for testing. The app package provides a helper for mocking Echo context.
Mocking Echo Context — github.com/labstack/echo/v5/echotest
ctx := echotest.ContextConfig{
Headers: http.Header{
echo.HeaderContentType: []string{echo.MIMEApplicationJSON},
},
JSONBody: []byte(`{"firstName":"john","lastName":"doe"}`),
}.ToContext(t)
ctx, rec := echotest.ContextConfig{
Headers: http.Header{
echo.HeaderContentType: []string{echo.MIMEApplicationJSON},
},
JSONBody: []byte(`{"firstName":"john","lastName":"doe"}`),
}.ToContextRecorder(t)The function returns:
echo.Context— for passing to handlers*httptest.ResponseRecorder— for asserting HTTP response
Example
func TestGetUser(t *testing.T) {
ctx, rec := echotest.ContextConfig{
Headers: http.Header{
echo.HeaderContentType: []string{echo.MIMEApplicationJSON},
},
JSONBody: []byte(`{"firstName":"john","lastName":"doe"}`),
}.ToContextRecorder(t)
handler := NewHandler(mockStorage)
err := handler.GetUser(ctx)
require.NoError(t, err)
assert.Equal(t, 200, rec.Code)
assert.JSONEq(t, `{"code":"0000","success":true,"data":{...}}`, rec.Body.String())
}Dependecy injection
Use mockery for generating mocks of interfaces. This allows you to easily create mock implementations of your interfaces for testing.
- add directive
//mockery:generate: trueto interface:
//mockery:generate: true
type Storager interface {
Users() ([]User, error)
UserByName(name string) (User, error)
CreateUser(user User) error
}- Install mockery:
go install github.com/vektra/mockery/v2@latest- Run mock generation:
mockery
Database testing
- Use
modernc.org/sqlitefor testing database interactions. This allows you to create an in-memory SQLite database for testing purposes, which is fast and does not require any setup.
import (
_ "modernc.org/sqlite"
"github.com/jmoiron/sqlx"
)
func TestDatabase(t *testing.T) {
db, err := sqlx.Open("sqlite", ":memory:")
require.NoError(t, err)
defer db.Close()
// Run migrations or setup schema here
// Perform database operations and assertions
}- Use
github.com/DATA-DOG/go-sqlmockfor testing database interactions without an actual database. This allows you to mock database queries and responses, making it easier to test your database logic in isolation.
import (
"github.com/DATA-DOG/go-sqlmock"
)
func TestDatabase(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
// Setup expected queries and responses here
// Perform database operations and assertions
}