name: go-3layer-architecture description: Implement clean 3-layer architecture for Go REST APIs with handlers, services, and infrastructure separation. Use when setting up new Go projects, refactoring applications, or establishing clear separation of concerns with proper folder structure.
Go 3-Layer Architecture
Implement a clean 3-layer architecture pattern for Go REST APIs with proper separation of concerns.
Architecture Pattern
┌─────────────────────────────────────────┐
│ Handlers (HTTP Layer) │
│ - Request parsing & validation │
│ - Response formatting │
│ - HTTP status codes │
├─────────────────────────────────────────┤
│ Services (Business Logic) │
│ - Domain logic │
│ - Data transformation │
│ - Returns DTOs │
├─────────────────────────────────────────┤
│ Infrastructure (Frameworks) │
│ - HTTP server (Gin) │
│ - Logging │
│ - Observability │
│ - Database clients │
└─────────────────────────────────────────┘
Folder Structure
CRITICAL: No .go files directly in internal/infrastructure/ root!
internal/
├── handlers/ # HTTP layer
│ ├── user_handler.go
│ └── response.go # Standard response types
├── services/ # Business logic
│ └── user_service.go
└── infrastructure/ # Frameworks (organized by concern)
├── server/
│ └── server.go
├── logging/
│ └── logger.go
└── observability/
└── otel.go
Implementation Steps
1. Create Folder Structure
mkdir -p internal/{handlers,services,infrastructure/{server,logging,observability}}
2. Define Response Types
// internal/handlers/response.go
package handlers
type BaseResponse struct {
Message string `json:"message"`
}
type DataResponse struct {
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
3. Create Service with DTOs
// internal/services/user_service.go
package services
type UserService struct {
// dependencies
}
func NewUserService() *UserService {
return &UserService{}
}
// DTO - always suffixed with DTO and returned as pointer
type UserDTO struct {
ID string `json:"id"`
Name string `json:"name"`
}
func (s *UserService) GetUser(id string) (*UserDTO, error) {
return &UserDTO{
ID: id,
Name: "John Doe",
}, nil
}
4. Create Handler
// internal/handlers/user_handler.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"your-project/internal/services"
)
type UserHandler struct {
userService *services.UserService
}
func NewUserHandler(userService *services.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
func (h *UserHandler) GetUser(c *gin.Context) {
id := c.Param("id")
user, err := h.userService.GetUser(id)
if err != nil {
c.JSON(http.StatusInternalServerError, BaseResponse{
Message: "Failed to get user",
})
return
}
c.JSON(http.StatusOK, DataResponse{Message: "ok", Data: user})
}
5. Setup Router in Infrastructure
// internal/infrastructure/server/server.go
package server
import (
"github.com/gin-gonic/gin"
"your-project/internal/handlers"
)
func SetupRouter(userHandler *handlers.UserHandler) *gin.Engine {
router := gin.New()
router.Use(gin.Recovery())
userGroup := router.Group("/users")
{
userGroup.GET("/:id", userHandler.GetUser)
}
return router
}
6. Wire Dependencies in Main
// cmd/main.go
package main
import (
"your-project/internal/handlers"
"your-project/internal/services"
"your-project/internal/infrastructure/server"
)
func main() {
// Initialize bottom-up (dependencies first)
userService := services.NewUserService()
userHandler := handlers.NewUserHandler(userService)
router := server.SetupRouter(userHandler)
router.Run(":8080")
}
Rules
✅ DO
- Keep handlers focused on HTTP concerns only
- Put ALL business logic in services
- Return pointers to DTOs from services (
*DTO) - Use constructor injection
- Organize infrastructure by concern in subfolders
- Wire all dependencies in main.go
❌ DON'T
- Put business logic in handlers
- Put
.gofiles directly ininternal/infrastructure/ - Mix layers (handlers calling handlers)
- Use global variables for dependencies
- Return DTO values (always return
*DTO) - Skip the DTO suffix on transfer objects
Code Quality
Linting
All code should pass golangci-lint checks:
# Run linters
golangci-lint run
# Auto-fix formatting
golangci-lint run --fix
Key requirements:
- All exported types and functions must have documentation comments ending with periods
- All packages must have package comments
- Error handling must use
errors.Isorerrors.As(not type assertions) - HTTP servers must have
ReadHeaderTimeoutto prevent Slowloris attacks - Code must be formatted with
gofmt/gofumpt - Imports must be organized with
goimports
Documentation Style
// Package handlers provides HTTP request handlers for API endpoints.
package handlers
// NewUserHandler creates a new user handler with the given dependencies.
func NewUserHandler(service *services.UserService) *UserHandler {
return &UserHandler{service: service}
}