Structuring Go application in a modular way

In this article we will structure a Go application in a modular fashion. We will only discuss the important parts of the example project.

The structure

The image below shows the structure of the application.

Entry point

Let’s start with the application entry point the main.go file. It is located inside of the cmd directory of the project.

package mainimport "txp/restapistarter/app"func main() {
app := new(app.App)
app.InitComponents()
app.Run()
}

The ‘app’ Package

The ‘app’ package consists of the app.go, router.go files and the module directory.

app.go

package appimport (
"log"
"net/http"
"txp/restapistarter/app/module/content"
"txp/restapistarter/app/module/user"
)
// global var
var (
UserModule *user.UserModule
ContentModule *content.ContentModule
)
// App struct
type App struct {
router *Router
}
func (a *App) initModules() {
UserModule = new(user.UserModule)
UserModule.InitComponents()
ContentModule = new(content.ContentModule)
ContentModule.InitComponents()
}
// Init app
func (a *App) InitComponents() {
a.initModules()
a.router = NewRouter()
}
// Run app
func (a *App) Run() {
err := http.ListenAndServe(
"127.0.0.1:8080",
a.router.Mux,
)
if err != nil {
log.Fatal(err)
}
}

router.go

package appimport (
"txp/restapistarter/pkg/middleware"
"txp/restapistarter/pkg/util"
"github.com/go-chi/chi"
)
// Router struct
type Router struct {
Mux *chi.Mux
}
func NewRouter() *Router {
r := &Router{}
r.Mux = chi.NewRouter()
r.registerMiddlewares()
r.registerUserRoutes(
util.V1,
)
r.registerContentRoutes(
util.V1,
)
return r
}
func (r *Router) registerMiddlewares() {
r.Mux.Use(
middleware.JSONContentTypeMiddleWare,
)
}
func (r *Router) registerUserRoutes(
version string,
) {
r.Mux.Route(
util.ApiPattern+version+util.UsersPattern,
func(r chi.Router) {
r.Get(
util.RootPattern,
UserModule.UserHandler.ReadMany,
)
r.Get(
util.RootPattern+"{id}",
UserModule.UserHandler.ReadOne,
)
r.Post(
util.RootPattern,
UserModule.UserHandler.Create,
)
r.Patch(
util.RootPattern+"{id}",
UserModule.UserHandler.Update,
)
r.Delete(
util.RootPattern+"{id}",
UserModule.UserHandler.Delete,
)
},
)
}
func (r *Router) registerContentRoutes(
version string,
) {
r.Mux.Route(
util.ApiPattern+version+util.ContentsPattern,
func(r chi.Router) {
r.Get(
util.RootPattern,
ContentModule.ContentHandler.ReadMany,
)
r.Get(
util.RootPattern+"{id}",
ContentModule.ContentHandler.ReadOne,
)
r.Post(
util.RootPattern,
ContentModule.ContentHandler.Create,
)
r.Patch(
util.RootPattern+"{id}",
ContentModule.ContentHandler.Update,
)
r.Delete(
util.RootPattern+"{id}",
ContentModule.ContentHandler.Delete,
)
},
)
}

The ‘module’ Directory

‘module’ directory contains contents and users directories. These are the two modules of the app.

The ‘user’ Package

This package contains the module setup code, handler, service, repository and other components of the user module.

user_module.go

package usertype UserModule struct {
UserHandler *UserHandler
UserService *UserService
UserRepository *UserRepository
}
func (m *UserModule) InitComponents() {
m.UserRepository = new(UserRepository)
m.UserService = NewUserService(
m.UserRepository,
)
m.UserHandler = NewUserHandler(
m.UserService,
)
}

user_handler.go

package userimport (
"net/http"
)
type UserHandler struct {
service *UserService
}
func NewUserHandler(
service *UserService,
) *UserHandler {
h := new(UserHandler)
h.service = service
return h
}
func (h *UserHandler) Create(
w http.ResponseWriter,
r *http.Request,
) {
h.service.Create(
w,
r,
)
}
func (h *UserHandler) ReadMany(
w http.ResponseWriter,
r *http.Request,
) {
h.service.ReadMany(
w,
r,
)
}
func (h *UserHandler) ReadOne(
w http.ResponseWriter,
r *http.Request,
) {
h.service.ReadOne(
w,
r,
)
}
func (h *UserHandler) Update(
w http.ResponseWriter,
r *http.Request,
) {
h.service.Update(
w,
r,
)
}
func (h *UserHandler) Delete(
w http.ResponseWriter,
r *http.Request,
) {
h.service.Delete(
w,
r,
)
}

user_service.go

package userimport (
"encoding/json"
"errors"
"log"
"net/http"
"txp/restapistarter/app/module/user/dto"
"txp/restapistarter/app/module/user/entity"
"txp/restapistarter/pkg/data"
"txp/restapistarter/pkg/util"
"github.com/go-chi/chi"
)
type UserService struct {
repository *UserRepository
}
func NewUserService(repository *UserRepository) *UserService {
s := new(UserService)
s.repository = repository
return s
}
func (s *UserService) Create(w http.ResponseWriter, r *http.Request) {
var b *dto.CreateUpdateUserDto
err := json.NewDecoder(r.Body).Decode(&b)
if err != nil {
util.RespondError(http.StatusBadRequest, err, w)
return
}
lastId, err := s.repository.Create(
&entity.User{
Name: b.Name,
},
)
if err != nil || lastId != "" {
util.RespondError(
http.StatusInternalServerError,
errors.New(util.InternalServerError),
w,
)
return
}
log.Print(lastId)
util.Respond(http.StatusCreated, b, w)
}
func (s *UserService) ReadMany(w http.ResponseWriter, r *http.Request) {
rows, err := s.repository.ReadMany()
if err != nil {
util.RespondError(
http.StatusInternalServerError,
err,
w,
)
return
}
var e entity.User
d, err := data.GetEntities(
rows,
&e,
&e.Id,
&e.Name,
&e.CreatedAt,
&e.UpdatedAt,
)
if err != nil {
util.RespondError(
http.StatusInternalServerError,
err,
w,
)
return
}
util.Respond(http.StatusOK, d, w)
}
func (s *UserService) ReadOne(w http.ResponseWriter, r *http.Request) {
userId := chi.URLParam(r, util.UrlKeyId)
row := s.repository.ReadOne(userId)
if row == nil {
util.RespondError(
http.StatusInternalServerError,
errors.New(util.InternalServerError),
w,
)
return
}
e := new(entity.User)
d, err := data.GetEntity(
row,
&e,
&e.Id,
&e.Name,
&e.CreatedAt,
&e.UpdatedAt,
)
if err != nil {
util.RespondError(
http.StatusInternalServerError,
err,
w,
)
return
}
util.Respond(http.StatusOK, d, w)
}
func (s *UserService) Update(w http.ResponseWriter, r *http.Request) {
userId := chi.URLParam(r, util.UrlKeyId)
var b *dto.CreateUpdateUserDto
err := json.NewDecoder(r.Body).Decode(&b)
if err != nil {
util.RespondError(
http.StatusBadRequest,
err,
w,
)
return
}
rowsAffected, err := s.repository.Update(
userId,
&entity.User{
Name: b.Name,
},
)
if err != nil || rowsAffected <= 0 {
util.RespondError(
http.StatusInternalServerError,
errors.New(util.InternalServerError),
w,
)
return
}
util.Respond(http.StatusOK, b, w)
}
func (s *UserService) Delete(w http.ResponseWriter, r *http.Request) {
userId := chi.URLParam(r, util.UrlKeyId)
rowsAffected, err := s.repository.Delete(userId)
if err != nil || rowsAffected <= 0 {
util.RespondError(
http.StatusInternalServerError,
errors.New(util.InternalServerError),
w,
)
return
}
util.Respond(
http.StatusOK,
map[string]bool{"success": true},
w,
)
}

user_repository.go

package userimport (
"database/sql"
"fmt"
"log"
"txp/restapistarter/app/module/user/entity"
"txp/restapistarter/pkg/data"
)
type UserRepository struct {
}
func (r *UserRepository) Create(
e *entity.User,
) (string, error) {
var lastId string = ""
result, err := data.DB.Exec(
"INSERT INTO users (name)"+
"VALUES ($1) RETURNING id",
e.Name,
)
if err != nil {
log.Println(err)
return lastId, err
}
if err != nil {
log.Println(err)
}
temp, _ := result.LastInsertId()
lastId = fmt.Sprintf("%d", temp)
return lastId, nil
}
func (r *UserRepository) ReadMany() (*sql.Rows, error) {
rows, err := data.DB.Query(
"SELECT * FROM users", // WHERE id IS NOT NULL
)
if err != nil {
return nil, fmt.Errorf("ReadMany %v", err)
}
return rows, nil
}
func (r *UserRepository) ReadOne(id string) *sql.Row {
row := data.DB.QueryRow(
"SELECT * FROM users WHERE id = $1 LIMIT 1",
id,
)
return row
}
func (r *UserRepository) Update(
id string,
e *entity.User,
) (int64, error) {
q := "UPDATE users SET name = $2 WHERE id = $1"
res, err := data.DB.Exec(
q,
id,
e.Name,
)
if err != nil {
log.Println(err)
return -1, err
}
return data.GetRowsAffected(res), nil
}
func (r *UserRepository) Delete(id string) (int64, error) {
q := "DELETE FROM users WHERE id = $1"
res, err := data.DB.Exec(
q,
id,
)
if err != nil {
log.Println(err)
return -1, err
}
return data.GetRowsAffected(res), nil
}

The source code is available here: https://github.com/tanveerprottoy/rest-api-starter-go

That’s it for this article. Please don’t forget to add some claps.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tanveer Prottoy

Tanveer Prottoy

I am complete with the precious one