Go Clean Architecture

repository·develop·Indexed 41 days ago

https://github.com/wesionaryteam/go_clean_architecture

A scalable production backbone implementation of Clean Architecture in Go. It utilizes the Gin Web Framework, uber-go/fx for dependency injection, GORM for ORM, and Atlas for declarative database migrations.

Tokens
6.5K
Snippets
24
Records
28
Agent score
100%

What's inside go_clean_architecture

  1. Understand the project folder structure

    develop

    The project follows a Clean Architecture pattern. Key directories include:

    • /bootstrap: Modules required to start the application.
    • /console: Server commands (run go run main.go -help for details).
    • /domain: Core business logic. Contains models, constants, and sub-folders for each domain (e.g., /domain/user) containing its controller, repository, routes, and services.
    • /domain/models: GORM ORM models.
    • /migrations: Database migration files managed by Atlas.
    • /pkg: Shared packages:
      • /pkg/errorz: Custom error types and handlers.
      • /pkg/framework: Core components (env parsing, logger).
      • /pkg/infrastructure: Third-party connections (AWS, DB, router).
      • /pkg/middlewares: HTTP middlewares.
      • /pkg/responses: Standardized HTTP response structures.
      • /pkg/services: Shared clients for external services (S3, SES, etc.).
    • /tests: Unit and integration tests.
    • main.go: Application entry point.
  2. Add and migrate new database models

    develop
    1. Define Models: Add new model definitions to the domain/models folder using GORM tags.
    2. Handle UUIDs: For UUID fields, use types.BinaryUUID. You can implement a BeforeCreate hook to automatically generate a new UUID if one isn't provided.
    3. Generate Migrations: Use atlas go to diff your models against the current database. The project provides make commands to simplify this.
    4. Apply Migrations: Run the generated migrations to update the database schema.

    Note: The project uses MySQL; ensure model definitions are compatible with MySQL syntax.

    package models
    
    import (
    	"clean-architecture/domain/constants"
    	"clean-architecture/pkg/types"
    
    	_ "ariga.io/atlas-provider-gorm/gormschema"
    
    	"github.com/google/uuid"
    	"gorm.io/gorm"
    )
    
    // User model
    type User struct {
    	gorm.Model
    	UUID       types.BinaryUUID `json:"uuid" gorm:"index;notnull;unique"`
    	CognitoUID *string          `json:"-" gorm:"index;size:50;unique"`
    
    	FirstName   string `json:"first_name" gorm:"size:255"`
    	LastName    string `json:"last_name" gorm:"size:255"`
    
    	Email string             `json:"email" gorm:"notnull;index,unique;size:255"`
    	Role  constants.UserRole `json:"role" gorm:"size:25" copier:"-"`
    }
    
    // BeforeCreate auto generate uuid before creating if it's not present already
    func (u *User) BeforeCreate(tx *gorm.DB) error {
    	if u.UUID.String() == (types.BinaryUUID{}).String() {
    		id, err := uuid.NewRandom()
    		u.UUID = types.BinaryUUID(id)
    		return err
    	}
    	return nil
    }
    
    func (*User) TableName() string {
    	return "users"
    }
  3. Setup the project environment

    develop

    Before running the application, you must initialize your environment variables by copying the example file and updating the required credentials.

    1. Copy the example environment file: cp .env.example .env
    2. Open the .env file and update your database credentials (DB_USER, DB_PASS, DB_NAME, DB_FORWARD_PORT).
    3. Update STORAGE_BUCKET_NAME with your AWS S3 bucket name.
    cp .env.example .env
  4. Discover available dependency updates

    develop

    You can check for available updates using the go list command.

    To list all dependencies of your current module along with their latest available versions, use go list -m -u all.

    To check the latest version for a single specific module, provide its path to the command.

    # List all dependencies and available updates
    go list -m -u all
    
    # Check update for a specific module
    go list -m -u example.com/theirmodule
  5. Define and register feature routes

    develop

    Routes are defined in a route.go file within the feature directory.

    1. Define Route Struct: The Route struct should hold dependencies like framework.Logger, infrastructure.Router, and the feature's *Controller.
    2. Initialize Route: Create a NewRoute constructor.
    3. Implement Registration: Create a RegisterRoute function that uses the router's Group method (e.g., r.handler.Group("/api")) to associate paths with controller methods.
    4. Automatic Registration: Use fx.Invoke(RegisterRoute) in your feature's fx.Module to ensure routes are registered automatically on application startup.
    package user
    
    import (
    	"clean-architecture/pkg/framework"
    	"clean-architecture/pkg/infrastructure"
    )
    
    // Route struct
    type Route struct {
    	logger     framework.Logger
    	handler    infrastructure.Router
    	controller *Controller
    }
    
    // NewRoute initializes a new Route instance
    func NewRoute(
    	logger framework.Logger,
    	handler infrastructure.Router,
    	controller *Controller,
    ) *Route {
    	return &Route{
    		handler:    handler,
    		logger:     logger,
    		controller: controller,
    	}
    }
    
    // RegisterRoute sets up the routes for the feature
    func RegisterRoute(r *Route) {
    	r.logger.Info("Setting up routes")
    
    	api := r.handler.Group("/api")
    
    	api.POST("/user", r.controller.CreateUser)
    	api.GET("/user/:id", r.controller.GetUserByID)
    }
  6. Update project dependencies

    develop

    To update your Go module dependencies to their latest minor/patch versions and clean up the go.mod file, follow these three steps:

    1. Run go get -u to update dependencies.
    2. Manually remove any dependency packages in your modules that are marked with // indirect.
    3. Run go mod tidy to synchronize the go.mod and go.sum files.
    go get -u
    # (Remove // indirect dependencies manually)
    go mod tidy
  7. Define and use common errors with the `errorz` package

    develop

    The errorz package allows you to define commonly used errors in a centralized manner. This ensures consistency in error messages and status codes across the application.

    1. Define Common Errors

    Create a package (e.g., errorz) and define APIError instances for frequent scenarios like unauthorized access or invalid input.

    package errorz
    
    import "net/http"
    
    var (
        ErrUnauthorized = &APIError{
            StatusCode: http.StatusUnauthorized,
            Message:    "Unauthorized access",
        }
    
        ErrResourceNotFound = &APIError{
            StatusCode: http.StatusNotFound,
            Message:    "The requested resource was not found",
        }
    
        ErrInvalidInput = &APIError{
            StatusCode: http.StatusBadRequest,
            Message:    "Invalid input provided",
        }
    )

    2. Use Defined Errors in Handlers

    Inject these errors into the responses.HandleError function within your handlers to return the standardized response.

    import (
        "github.com/gin-gonic/gin"
        "clean-architecture/pkg/errorz"
        "clean-architecture/pkg/framework"
        "clean-architecture/pkg/responses"
        "net/http"
    )
    
    func ExampleHandler(c *gin.Context) {
        logger := framework.NewLogger()
    
        // Simulate an error condition
        if true { 
            responses.HandleError(logger, c, errorz.ErrInvalidInput)
            return
        }
    
        responses.SuccessJSON(c, http.StatusOK, "Operation successful")
    }
  8. Capture unhandled exceptions with Sentry

    develop

    To capture unhandled exceptions (panics) in your application, use a defer block with recover() to intercept the panic. Once recovered, use utils.CurrentSentryService.CaptureException(err) to send the error details to Sentry for debugging. This ensures that unexpected crashes are logged with context rather than silently failing or crashing the process without a trace.

    Best Practices:

    1. Always use defer and recover to handle panics.
    2. Pass relevant context to Sentry to facilitate easier debugging.
    3. Security: Ensure that sensitive user information or credentials are not included in the error context sent to Sentry.
    import (
    	"errors"
    	"github.com/gin-gonic/gin"
    	"clean-architecture/pkg/utils"
    )
    
    func ExampleUnhandledExceptionHandler(c *gin.Context) {
    	defer func() {
    		if r := recover(); r != nil {
    			err := errors.New("Unhandled exception occurred")
    			utils.CurrentSentryService.CaptureException(err)
    			c.JSON(500, gin.H{"error": "An unexpected error occurred. Please try again later."})
    		}
    	}()
    
    	// Simulate an unhandled exception
    	panic("Simulated panic")
    }
  9. How to add a new API endpoint feature

    develop

    To implement a new feature, follow these architectural steps:

    1. Create Feature Directory: Create a new directory under domain/<feature_name>/.
    2. Implement Components: Create separate files for the following components within that directory:
      • controller.go
      • route.go
      • service.go
      • module.go
      • serializer.go (DTO layer)
      • repository.go
    3. Define DTOs: Strictly separate the database structure (defined in models/) from the request/response structure. Use the DTO layer in your feature directory to convert between these two.
    4. Dependency Injection: In domain/<feature_name>/module.go, set up the dependency injection using fx.Module. Use fx.Provide for your components and fx.Invoke for route registration.
    5. Link Module: Ensure the new feature's module is linked to the main domain/module.go so it is included in the application's dependency injection tree.
  10. Run tests and check coverage

    develop

    The framework includes built-in support for unit and integration testing.

    Run all tests

    Execute all tests in the project with verbose output: go test ./... -v

    Generate test coverage report

    To generate a coverage profile and view it as an HTML report:

    1. Run tests with coverage profiling: go test ./... -v -coverprofile cover.txt -coverpkg=./...
    2. Convert the profile to an HTML file: go tool cover -html=cover.txt -o index.html
    go test ./... -v