Skip to content

A CLI tool for initializing and managing hexagonal Go projects.

License

Notifications You must be signed in to change notification settings

ksckaan1/hexago

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hexago

Hexago is a CLI tool for you to create a Go project by applying hexagonal architecture.

TOC

Installation

go install github.com/ksckaan1/hexago@latest

Dependencies

Warning

Make sure that the directory $HOME/go/bin is appended to the $PATH environment variable

Before We Start

If you didn’t hear about hexagonal architecture before, firstly, you could research about it.

Here it is nice blog posts about hexagonal architecture:

Why Hexago?

Hexago can be used to create hexagonal Go projects in an organised way. In this way, you can follow certain standards and have a more manageable application development phase. It imposes its own rules for certain situations and as a result of these impositions, your project gains regularity.

You can also use Hexago only for creating hexagonal projects. It is your preference whether or not to bring it with hexago.

Commands

doctor

The doctor command displays the status of dependencies that are required for hexago to run properly.

Example:

init

The init command initialize a Hexago project. This command creates a domain named core by default. Promts go module name. If leaves blank, uses project folder name as lowercase defaultly.

hexago init <project-path>

Example:

domain

This is the parent command for all domain-related operations.

If the project does not contain any domain, a new service and app cannot be created. For this, a domain must be created first.

  • new

    This command creates a new domain under the internal/domain directory.

    hexago domain new

  • ls

    This command lists all domains under the internal/domain directory.

    hexago domain ls

    Flags:

    • -l: lists domains line-by-line

port

This is the parent command for all port-related operations.

Ports can be implemented when creating service, app, infrastructure and package. If there is no port in the project, it is not asked which port to implement in the creation screen.

You can create a port manually like bellow.

// internal/port/user.go

package port

type UserController interface {
  CreateUser(w http.ResponseWriter, r *http.Request)
  GetUserByID(w http.ResponseWriter, r *http.Request)
  GetAllUsers(w http.ResponseWriter, r *http.Request)
  UpdateUserByID(w http.ResponseWriter, r *http.Request)
  DeleteUserByID(w http.ResponseWriter, r *http.Request)
}

type UserService interface {
  CreateUser(ctx context.Context, params dto.CreateUserParams) (string, error)
  GetUserByID(ctx context.Context, userID string) (*dto.User, error)
  GetAllUsers(ctx context.Context) ([]*dto.User, error)
  UpdateUserByID(ctx context.Context, params dto.UpdateUserParams) error
  DeleteUserByID(ctx context.Context, userID string) error
}

type UserRepository interface {
  CreateUser(ctx context.Context, params dto.CreateUserParams) (string, error)
  GetUserByID(ctx context.Context, userID string) (*dto.User, error)
  GetAllUsers(ctx context.Context) ([]*dto.User, error)
  UpdateUserByID(ctx context.Context, params dto.UpdateUserParams) error
  DeleteUserByID(ctx context.Context, userID string) error
}

You can use this port when creating a new service, app, infrastructure or package.

  • ls:

    This command lists all ports under the internal/port directory.

    Flags:

    • -l: lists ports line-by-line

service

This is the parent command for all service-related (domain-service) operations.

  • new

    This command creates a new service under the internal/domain/<domainname>/service/<servicename> directory.

    Domain is required to create a service. Steps applied when creating a service:

    • Insert service name (PascalCase)
    • Insert folder name (lowercase)
    • Select a domain
    • Select port which will be implemented (skips this step if there is no port)
    • Assert port if selected
    hexago service new

  • ls

    This command lists all services under the internal/domain/<domainname>/service directory.

    hexago service ls

    Flags:

    • -l: lists services line-by-line

app

This is the parent command for all application-related (application-service) operations.

Application services are the places where endpoints such as controllers or cli applications are hosted.

  • new

    This command creates a new application under the internal/domain/<domainname>/app/<appname> directory.

    Domain is required to create an application. Steps applied when creating an application:

    • Insert application name (PascalCase)
    • Insert folder name (lowercase)
    • Select a domain
    • Select port which will be implemented (skips this step if there is no port)
    • Assert port if selected
    hexago app new

  • ls

    This command lists all applications under the internal/domain/<domainname>/app directory.

    hexago app ls

    Flags:

    • -l: lists applications line-by-line

infra

This is the parent command for all infrastructure-related operations.

Infrastructures host databases (repositories), cache adapters or APIs that we depend on while writing applications

  • new

    This command creates a new infrastructure under the internal/infrastructure/<infraname> directory.

    Steps applied when creating an infrastructure:

    • Insert infrastructure name (PascalCase)
    • Insert folder name (lowercase)
    • Select port which will be implemented (skips this step if there is no port)
    • Assert port if selected
    hexago infra new

  • ls

    This command lists all infrastructures under the internal/infrastructure directory.

    hexago infra ls

    Flags:

    • -l: lists infrastructures line-by-line

pkg

This is the parent command for all package-related operations.

Packages are the location where we host features such as utils. There are two types of packages in a hexago project.

  • The first one is located under /internal/pkg and is not imported by other go developers. Only you use these packages in the project.

  • The second is located under /pkg. The packages here can be used both by your project and by other go developers.

  • new

    This command creates a new package under the internal/pkg/<pkgname> or /pkg/<pkgname> directory.

    Steps applied when creating a package:

    • Insert package name (PascalCase)
    • Insert folder name (lowercase)
    • Select port which will be implemented (skips this step if there is no port)
    • Assert port if selected
    • Select package scope (global or internal)
    hexago pkg new

  • ls

    This command lists all packages under the internal/pkg or /pkg directory.

    hexago pkg ls # list internal packages

    Flags:

    • -g: lists global packages
    • -a: list both global and internal packages.
    • -l: lists packages line-by-line

cmd

This is the parent command for all entry point-related (cmd) operations.

Entry points are the places where a go application will start running. entry points are located under the cmd directory.

  • new

    This command creates a new entry point under the cmd/<entry-point-name> directory.

    There is only one step creating an entry point.

    • Insert entry point folder name (kebab-case)

    Creates a go file like bellow.

    package main
    
    import (
      "fmt"
      "os"
      "path/filepath"
      "strings"
    )
    
    func main() {
      fmt.Printf("Hello from %s!\n", strings.ToUpper(filepath.Base(os.Args[0])))
      if env, ok := os.LookupEnv("MY_ENV"); ok {
        fmt.Println("MY_ENV ->", env)
      }
    }
    hexago cmd new

  • ls

    This command lists all entry points under the cmd directory.

    hexago cmd ls

    Flags:

    • -l: lists entry points line-by-line

run

This command can be used for two different purposes. the run command create a log file under the logs directory defaultly.

  • Firstly, if there is an entry point in your project, it can be used to run this entry point.

    hexago run <entry-point-name>

    Flags:

    • -e: run entry point with environment variable. You can use multiple environment variable
      hexago run <entry-point-name> -e <ENV_KEY1>=<ENV_VALUE1> -e <ENV_KEY2>=<ENV_VALUE2>

    You can customize this run command with given entry point in .hexago/config.yaml file.

    You can specify all envs from config.yaml file like bellow.

    templates: # std | do | <custom>
    service: std
    application: std
    infrastructure: std
    package: std
    
    runners:
      api: # it runs "go run ./cmd/api", if exists
        env:
          - ENV_KEY1=ENV_VAL1
          - ENV_KEY2=ENV_VAL2
        log:
          disable: false # write logs to files
          seperate_files: true # create log files seperately as api.stderr.log and api.stdout.log
          overwrite: true # create new log file when runner called

    When the hexago run api command is executed as above, it starts the api entry point according to the settings in the config.yaml file.

  • As a second method, you can use the run sub-command as an alternative to makefile. You can create a new entry in the runners section of the .hexago/config.yaml file to call it with the run command.

    The special commands created do not need to have an entry point equivalent. We can add a special command using the cmd key.

    runners:
      custom-command:
        cmd: "go version" # overwrite default "go run ./cmd/mycommand/" command
        log:
          disabled: true # do not print log file

    When you run hexago run custom-command command, you will get the following result.

    go version go1.23.0 darwin/arm64
    

tree

This command prints hexagonal structure of project.

hexago tree

Templates

When creating service, application, infrastructure and package with Hexago, templates are used to create go files. Hexago has 2 built-in templates, std and do.

std template uses the standard go instance initialiser.

package mypkg

type MyPkg struct{}

func New() (*MyPkg, error) {
  return &MyPkg{}, nil
}

do is a package that provides a dependency injection container.

package mypkg

import "github.com/samber/do"

type MyPkg struct{}

func New(i *do.Injector) (*MyPkg, error) {
  return &MyPkg{}, nil
}

Which template to use can be determined in the .hexago/config.yaml file.

templates: # std | do | <custom>
  service: std
  application: do
  infrastructure: std
  package: do

Custom Templates

If you want to use another template other than these templates, you can create your own template.

Naming Custom Template

Custom templates are hosted under the .hexago/templates/ directory. There is a convention that must be used when naming the template file.

.hexago/templates/<template-name>_<template-type>.tmpl

Examples:

  • .hexago/templates/abc_service.tmpl
  • .hexago/templates/abc_application.tmpl
  • .hexago/templates/abc_infrastructure.tmpl
  • .hexago/templates/abc_package.tmpl

Content of Custom Template

For example, the std template is as follows.

package {{.PkgName}}

{{if and .AssertInterface (ne .InterfaceName "") (ne .ImportPath "")}}
import "{{.ImportPath}}"

var _ port.{{.InterfaceName}} = (*{{.StructName}})(nil)
{{end}}

type {{.StructName}} struct{}

func New() (*{{.StructName}}, error) {
  return &{{.StructName}}{}, nil
}

{{ if ne .Implementation "" }}{{ .Implementation }}{{end}}

Selecting Custom Template to Use

To use the created custom template, you must specify the template name in config.yaml.

templates: # std | do | <custom>
  service: abc
  application: std
  infrastructure: std
  package: std