Backend Project Structure
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 triggerinit() - 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 ✅
- Keep controllers thin - delegate to services
- Use descriptive names -
CreateUsernotCreate - Group related code - all user endpoints in one controller
- Separate concerns - controllers, services, models
- Document exported functions - add comments
- Use consistent formatting - run
gofmtandgoimports
DON'T ❌
- Don't mix concerns - no business logic in controllers
- Don't use global variables - use dependency injection
- Don't ignore errors - handle all error cases
- Don't hardcode values - use configuration
- 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:
- Learn how to create controllers
- Understand business logic with services
- Work with database and models
- Implement error handling
Need help organizing your project? Ask in GitHub Discussions.