Effective Project Structure for Backend Projects in Go


Introduction
This article provides an optimal and modular project structure for backend projects in Golang, which I use as a starting template for my projects to ensure they are scalable and well maintained.
Along with the structure, it also includes a starter codebase built using gin
framework, which can be used as a template.
Why Project Structure Matters?
Designing the structure of a project is important right from the beginning of the development. As the project grows, so does the codebase & complexity, which needs to be distributed properly across well-thought folders and files.
A clear structure makes it easy for the developers to navigate through the code as it grows and also lets future developers focus on implementing new features rather than spending time searching for relevant code.
Folder Structure
/api
The api
folder consists of sub-folders to maintain the versioning of API to manage changes without breaking existing clients using previous versions.
├── api/
│ └── v1/
│ └── routes.go
│ └── userRoutes.go
The sub-folders for versions named as v1
, v2
, etc contains routes.go
files which basically registers different routes divided into different files based on entity/features.
// routes.go
package v1
import (
"database/sql"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(router *gin.Engine, db *sql.DB) {
registerUserRoutes(router, db)
}
// userRoutes.go
package v1
import (
"database/sql"
"github.com/Aniketyadav44/go-backend-template/internal/handlers"
"github.com/Aniketyadav44/go-backend-template/internal/services"
"github.com/gin-gonic/gin"
)
func registerUserRoutes(router *gin.Engine, db *sql.DB) {
userService := services.NewUserService(db)
userHandler := handlers.NewUserHandler(userService)
v1 := router.Group("/api/v1")
{
v1.GET("/users", userHandler.GetAllUsers)
}
}
Here, we basically created a service
and handler
instance by first injecting the database dependency in the service and then passing this service to the handler.
Then we defined a route group for /api/v1
path named as v1
and on this, we register our endpoint methods using the respective handler functions.
/cmd
The cmd
folder is used as the entry point of our backend application, which contains sub-folders depending on the type of application we are building.
e.g cmd/api
for our REST API or cmd/grpc
for a grpc service or cmd/cli
for an CLI application.
These sub-folders have their main.go
file which is the exact entry point file of the project.
├── cmd/
│ └── api/
│ └── main.go
// main.go
package main
import (
"log"
v1 "github.com/Aniketyadav44/go-backend-template/api/v1"
"github.com/Aniketyadav44/go-backend-template/internal/config"
"github.com/gin-gonic/gin"
)
func main() {
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal("error in loading config: ", err)
}
router := gin.Default()
v1.RegisterRoutes(router, cfg.DB)
if err := router.Run(":" + cfg.Port); err != nil {
log.Fatal("error in starting server: ", err)
}
}
Here, we first load our config instance which provides the port and database instance for this demo.
Next we create a Gin router instance and then we register our API routes from the v1
package in api/v1
on this router.
Finally, we start the server using router.Run()
binding it to the port we loaded from .env
in config.
/internal
the internal
folder consists of the application level logic further divided into sub folders depending on the configuration, control(presentation), business logic, data models and middlewares.
├── internal/
│ ├── config/
│ │ └── config.go
│ │ └── db.go
│ ├── handlers/
│ │ └── userHandler.go
│ ├── services/
│ │ └── userService.go
│ ├── models/
│ │ └── userModel.go
│ └── middleware/
│ └── authMiddleware.go
/internal/config
the config
sub-folder in internal
folder contains files for the config.go
file which loads the environment variables and initializes our database instance.
// config.go
package config
import (
"database/sql"
"os"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
)
type Config struct {
Port string
DB *sql.DB
}
func LoadConfig() (*Config, error) {
if err := godotenv.Load(); err != nil {
return nil, err
}
port := getEnvKey("PORT", "")
dbHost := getEnvKey("DB_HOST", "")
dbPort := getEnvKey("DB_PORT", "")
dbUser := getEnvKey("DB_USERNAME", "")
dbPassword := getEnvKey("DB_PASSWORD", "")
dbName := getEnvKey("DB_NAME", "")
db, err := loadDb(dbHost, dbPort, dbUser, dbPassword, dbName)
if err != nil {
return nil, err
}
return &Config{
Port: port,
DB: db,
}, nil
}
func getEnvKey(key, defaultValue string) string {
if val, exists := os.LookupEnv(key); exists {
return val
}
return defaultValue
}
In this, we have defined a Config
struct which holds the application level configurations. Specifically in this demo template, there is our database connection instance(*sql.DB
) and server port.
Then, there is a function LoadConfig()
which returns the loaded Config
along with any error. This function first loads the environment using godotenv
package and then loads database connection using loadDb()
function defined in /config/db.go
file.
In this db.go
file, we create our database connection instance using sql
package and test our connection using the db.Ping()
function
// db.go
package config
import (
"database/sql"
"fmt"
)
func loadDb(dbHost, dbPort, dbUser, dbPassword, dbName string) (*sql.DB, error) {
psConnStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dbUser, dbPassword, dbHost, dbPort, dbName)
db, err := sql.Open("postgres", psConnStr)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
Just like db.go
, it can contain configuration of different databases/services like redis
, kafka
, etc.
/internal/handlers
This folder consists of the HTTP handlers which defines how the requests will be processed.
// userHandler.go
package handlers
import (
"net/http"
"github.com/Aniketyadav44/go-backend-template/internal/services"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
service *services.UserService
}
func NewUserHandler(service *services.UserService) *UserHandler {
return &UserHandler{
service: service,
}
}
// ... handler functions defined here
func (h *UserHandler) GetAllUsers(c *gin.Context) {
users, err := h.service.GetAllUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"users": users})
}
Here, we have defined a UserHandler
struct which has a reference to it’s service
. This service
contains db connection injected which holds all of the business logic for specific handler functions to be called on specific routes.
Then, a constructor function NewUserHandler()
is defined which basically initializes and returns a new handler with service dependency injected.
/internal/services
This folder consists of the services which holds all of the business logic for specific handlers.
// userService.go
package services
import (
"database/sql"
"github.com/Aniketyadav44/go-backend-template/internal/models"
)
type UserService struct {
db *sql.DB
}
func NewUserService(db *sql.DB) *UserService {
return &UserService{
db: db,
}
}
func (s *UserService) GetAllUsers() ([]models.User, error) {
// ... get users logic from database connection -> s.db
}
Here, we have defined UserService
struct that holds a reference to the database connection (*sql.DB
).
Then, there is a constructor function NewUserService()
which initializes and returns new instance of service with the database connection injected.
/internal/models
This folder consists of the different data structures used across our application. This also contains the structures for our request body of POST APIs
// userModel.go example
package models
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
/internal/middlewares
This folder consists of the different middlewares used for our APIs divided into different files.
// authMiddleware.go
package middlewares
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// ...logic
c.Next()
}
}
/pkg
This folder consists of various sub-folders such as responses
, utils
, etc which holds all of the re-usable code to be imported across our application.
for e.g.
/pkg/utils/utils.go
can consist of common utilities functions/pkg/utils/crypto.go
can consist of the encryption, decryption, hashing, etc functions/pkg/responses/errorResponses.go
can consist of different error responses to be sent in error conditions.
Conclusion
Designing a clean and modular structure is very important for a scalable and maintainable codebase. By organizing code files into different logical layers, we make our project easier to understand and extend.
This gin
based template can be used to quickly get started with a new project.
This template can be found here:
If you find this article helpful, don't forget to hit the ❤️ button.
Check out my website here and feel free to connect.
Happy Coding! 👨💻
Subscribe to my newsletter
Read articles from Aniket Yadav directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
