Skip to main content

Backend Project Structure

Developer Beginner

Proper project organization is key to maintainability and scalability. This guide explains how to structure your EZ-Console backend application.

Standard Project Layout

your-project/
├── backend/
│ ├── controller/ # HTTP request handlers
│ │ ├── user_controller.go
│ │ ├── product_controller.go
│ │ └── order_controller.go
│ ├── service/ # Business logic
│ │ ├── user_service.go
│ │ ├── product_service.go
│ │ └── order_service.go
│ ├── model/ # Data models (optional, can extend framework models)
│ │ ├── product.go
│ │ └── order.go
│ ├── config/ # Configuration files (optional)
│ │ └── config.yaml
│ ├── main.go # Application entry point
│ ├── go.mod # Go module definition
│ └── go.sum # Dependency checksums
└── web/ # Frontend (separate directory)

Directory Responsibilities

/controller

Purpose: HTTP request handling

What goes here:

  • Controller structs
  • HTTP endpoint handlers
  • Request/response DTOs (Data Transfer Objects)
  • Route registration
  • Input validation
  • Response formatting

Example:

// controller/product_controller.go
package controller

type ProductController struct {
svc server.Service
}

func (c *ProductController) ListProducts(ctx *gin.Context) {
// Handle HTTP request
}

func (c *ProductController) RegisterRoutes(ctx context.Context, router *gin.RouterGroup) {
// Register routes
}

What should NOT go here:

  • ❌ Business logic
  • ❌ Database queries
  • ❌ Complex calculations
  • ❌ External API calls

/service

Purpose: Business logic and data access

What goes here:

  • Service structs
  • Business logic implementation
  • Database operations
  • Transaction management
  • Integration with external services
  • Data validation and transformation

Example:

// service/product_service.go
package service

type ProductService struct {
db *gorm.DB
}

func (s *ProductService) CreateProduct(ctx context.Context, req CreateProductRequest) (*model.Product, error) {
// Business logic and database operations
}

What should NOT go here:

  • ❌ HTTP request/response handling
  • ❌ gin.Context usage
  • ❌ HTTP status codes
  • ❌ Response formatting

/model

Purpose: Data structure definitions (optional)

This directory is optional. You can use framework models from github.com/sven-victor/ez-console/pkg/model or define your own custom models here.

What goes here:

  • Custom GORM models
  • Domain objects
  • Database schema definitions
  • Model methods and relationships

Example:

// model/product.go
package model

import (
"github.com/sven-victor/ez-console/pkg/model"
)

type Product struct {
model.Base
Name string `gorm:"size:128;not null" json:"name"`
Description string `gorm:"type:text" json:"description"`
Price float64 `gorm:"type:decimal(10,2)" json:"price"`
Stock int `gorm:"default:0" json:"stock"`
CategoryID uint `json:"-"`
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
}

func (Product) TableName() string {
return "products"
}

main.go

Purpose: Application entry point

This is where your application starts. It should be simple and clean.

Example:

// main.go
package main

import (
// Import controllers to trigger init() functions
_ "github.com/yourusername/your-project/controller"

consoleserver "github.com/sven-victor/ez-console/server"
)

const VERSION = "1.0.0"

var rootCmd = consoleserver.NewCommandServer(
"your-project",
VERSION,
"Your Project Description",
)

func main() {
rootCmd.Execute()
}

Key Points:

  • Import controller package with blank import _ to trigger init()
  • Use consoleserver.NewCommandServer() to create root command
  • Keep it minimal - don't add business logic here

Naming Conventions

File Names

Use lowercase with underscores:

✅ user_controller.go
✅ product_service.go
✅ order_model.go

❌ UserController.go
❌ productService.go
❌ Order-Model.go

Package Names

Use short, lowercase, single-word names:

✅ controller
✅ service
✅ model

❌ controllers
❌ userService
❌ product_models

Struct Names

Use PascalCase:

type UserController struct {}
type ProductService struct {}
type OrderModel struct {}

type userController struct {}
type product_service struct {}

Method Names

Exported methods (public): PascalCase

func (c *UserController) GetUser(ctx *gin.Context)
func (s *UserService) CreateUser(ctx context.Context, req CreateUserRequest)

Unexported methods (private): camelCase

func (c *UserController) validateEmail(email string) bool
func (s *UserService) hashPassword(password string) string

Code Organization Patterns

Pattern 1: Single File per Controller

For small projects or simple controllers:

controller/
├── user_controller.go # All user-related endpoints
├── product_controller.go # All product-related endpoints
└── order_controller.go # All order-related endpoints

Pattern 2: Directory per Module

For larger projects with many endpoints:

controller/
├── user/
│ ├── controller.go # Main controller struct
│ ├── create.go # Create user endpoint
│ ├── update.go # Update user endpoint
│ ├── delete.go # Delete user endpoint
│ └── list.go # List users endpoint
├── product/
│ ├── controller.go
│ ├── create.go
│ └── list.go
└── order/
└── controller.go

Pattern 3: Feature-Based Organization

For complex domains:

backend/
├── user/
│ ├── controller.go
│ ├── service.go
│ ├── model.go
│ └── dto.go
├── product/
│ ├── controller.go
│ ├── service.go
│ ├── model.go
│ └── dto.go
└── main.go

Import Organization

Organize imports in this order:

package controller

import (
// 1. Standard library
"context"
"net/http"
"time"

// 2. External packages
"github.com/gin-gonic/gin"
"gorm.io/gorm"

// 3. Framework packages
"github.com/sven-victor/ez-console/server"
"github.com/sven-victor/ez-console/pkg/model"
"github.com/sven-victor/ez-console/pkg/util"

// 4. Your project packages
"github.com/yourusername/your-project/service"
)

Use goimports to automatically organize imports:

goimports -w .

Configuration Files

Development Configuration

Create config.dev.yaml:

server:
host: "0.0.0.0"
port: 8080
mode: "debug"

database:
driver: "sqlite"
path: "./dev.db"

log:
level: "debug"
format: "logfmt"

Production Configuration

Create config.prod.yaml:

server:
host: "0.0.0.0"
port: 8080
mode: "release"

database:
driver: "postgres"
host: "db.example.com"
port: 5432
username: "${DB_USER}"
password: "${DB_PASSWORD}"
dbname: "production_db"

log:
level: "info"
format: "json"

Using Configuration

# Development
go run main.go --config config.dev.yaml --global.encrypt-key=dev-key-16bytes

# Production
go run main.go --config config.prod.yaml --global.encrypt-key=${ENCRYPT_KEY}

Dependency Management

Initialize Go Module

go mod init github.com/yourusername/your-project

Add Dependencies

# Core framework
go get github.com/sven-victor/ez-console@latest

# Additional dependencies
go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/sqlite
go get gorm.io/driver/mysql
go get gorm.io/driver/postgres

Keep Dependencies Updated

# Update all dependencies
go get -u ./...

# Update specific package
go get -u github.com/sven-victor/ez-console@latest

# Tidy up dependencies
go mod tidy

Vendor Dependencies (Optional)

For reproducible builds:

go mod vendor

Then build with:

go build -mod=vendor ./...

Environment Variables

Using .env Files

Create .env (don't commit to git):

# Server
SERVER_PORT=8080
SERVER_MODE=debug

# Database
DATABASE_DRIVER=sqlite
DATABASE_PATH=./dev.db

# Security
GLOBAL_ENCRYPT_KEY=dev-key-16-bytes

# External Services
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=your-app-password

Load with godotenv:

import "github.com/joho/godotenv"

func init() {
godotenv.Load()
}

Common Patterns

Request/Response DTOs

Create DTOs in controller files:

// controller/product_controller.go

type CreateProductRequest struct {
Name string `json:"name" binding:"required,min=1,max=128"`
Description string `json:"description" binding:"max=500"`
Price float64 `json:"price" binding:"required,min=0"`
Stock int `json:"stock" binding:"min=0"`
}

type UpdateProductRequest struct {
Name *string `json:"name" binding:"omitempty,min=1,max=128"`
Description *string `json:"description" binding:"omitempty,max=500"`
Price *float64 `json:"price" binding:"omitempty,min=0"`
Stock *int `json:"stock" binding:"omitempty,min=0"`
}

type ProductResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt time.Time `json:"created_at"`
}

Error Handling

Create custom errors in service:

// service/errors.go
package service

import "errors"

var (
ErrProductNotFound = errors.New("product not found")
ErrInsufficientStock = errors.New("insufficient stock")
ErrInvalidPrice = errors.New("invalid price")
)

Use in controller:

product, err := c.svc.Product().GetByID(ctx.Request.Context(), productID)
if err != nil {
if errors.Is(err, service.ErrProductNotFound) {
util.RespondWithError(ctx, util.NewErrorMessage("E4041", "Product not found"))
} else {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to get product", err))
}
return
}

Best Practices

DO ✅

  1. Keep controllers thin - delegate to services
  2. Use descriptive names - CreateUser not Create
  3. Group related code - all user endpoints in one controller
  4. Separate concerns - controllers, services, models
  5. Document exported functions - add comments
  6. Use consistent formatting - run gofmt and goimports

DON'T ❌

  1. Don't mix concerns - no business logic in controllers
  2. Don't use global variables - use dependency injection
  3. Don't ignore errors - handle all error cases
  4. Don't hardcode values - use configuration
  5. Don't circular dependencies - refactor if needed

Example: Complete Project Structure

product-manager/
├── backend/
│ ├── controller/
│ │ ├── product_controller.go
│ │ ├── category_controller.go
│ │ └── order_controller.go
│ ├── service/
│ │ ├── product_service.go
│ │ ├── category_service.go
│ │ ├── order_service.go
│ │ └── errors.go
│ ├── model/
│ │ ├── product.go
│ │ ├── category.go
│ │ └── order.go
│ ├── config/
│ │ ├── config.dev.yaml
│ │ └── config.prod.yaml
│ ├── .env
│ ├── .gitignore
│ ├── main.go
│ ├── go.mod
│ ├── go.sum
│ └── README.md
└── web/
└── ... (frontend code)

Next Steps

Now that you understand project structure:

  1. Learn how to create controllers
  2. Understand business logic with services
  3. Work with database and models
  4. Implement error handling

Need help organizing your project? Ask in GitHub Discussions.