🛠️ Development Guide¶
A comprehensive guide to understanding the codebase and building new features.
📑 Table of Contents¶
- Architecture Overview
- Directory Structure
- Understanding the Layers
- How User Management Works
- Adding New Features
- Best Practices
- Common Patterns
🏗️ Architecture Overview¶
This project follows Clean Architecture principles with clear separation of concerns:
┌─────────────────────────────────────────────────────────┐
│ HTTP Layer │
│ (Handlers - Receive requests, return responses) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Business Layer │
│ (Services - Business logic, orchestration) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Data Layer │
│ (Repositories - Database operations) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Database │
│ (PostgreSQL - Data storage) │
└─────────────────────────────────────────────────────────┘
Key Principles¶
- Separation of Concerns - Each layer has one responsibility
- Dependency Injection - Dependencies flow inward
- Interface-Based - Layers communicate through interfaces
- Testable - Each layer can be tested independently
- Maintainable - Easy to understand and modify
📁 Directory Structure¶
internal/
├── auth/ # Authentication & Authorization
│ ├── dto.go # JWT Claims
│ ├── service.go # Token generation/validation
│ └── middleware.go # Auth middleware for routes
│
├── config/ # Configuration Management
│ ├── config.go # Config structs and loading logic
│ └── config_test.go # Configuration tests
│
├── db/ # Database Connection
│ └── db.go # PostgreSQL connection setup
│
├── middleware/ # HTTP Middleware
│ ├── logger.go # Request logging middleware
│ ├── logger_test.go # Logger middleware tests
│ └── README.md # Middleware documentation
│
├── server/ # Server & Routing
│ └── router.go # Route definitions
│
└── user/ # User Domain (Example Feature)
├── model.go # Database model (GORM)
├── dto.go # Request/Response objects
├── repository.go # Database operations
├── service.go # Business logic
└── handler.go # HTTP handlers
File Responsibilities¶
File | Purpose | Contains |
---|---|---|
model.go | Database schema | GORM models with tags |
dto.go | Data transfer | Request/Response structs |
repository.go | Data access | CRUD operations |
service.go | Business logic | Validation, orchestration |
handler.go | HTTP layer | Route handlers, Swagger docs |
🔍 Understanding the Layers¶
1. Model Layer (model.go
)¶
Purpose: Define database schema using GORM
Example:
package user
import (
"gorm.io/gorm"
"time"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
PasswordHash string `gorm:"not null" json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
Key Points: - Use GORM tags for database constraints - Use JSON tags for API responses - Use json:"-"
to hide sensitive fields - gorm.DeletedAt
enables soft deletes
2. DTO Layer (dto.go
)¶
Purpose: Define request/response structures
Example:
package user
type RegisterRequest struct {
Name string `json:"name" binding:"required,min=2"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
type UserResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
Key Points: - Use binding
tags for validation - Never expose PasswordHash
in responses - Create separate structs for requests/responses - Use clear, descriptive names
3. Repository Layer (repository.go
)¶
Purpose: Handle all database operations
Example:
package user
import (
"gorm.io/gorm"
)
type UserRepository interface {
Create(user *User) error
FindByID(id uint) (*User, error)
FindByEmail(email string) (*User, error)
List() ([]User, error)
Update(user *User) error
Delete(id uint) error
}
type GormUserRepository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) UserRepository {
return &GormUserRepository{db: db}
}
func (r *GormUserRepository) Create(user *User) error {
return r.db.Create(user).Error
}
func (r *GormUserRepository) FindByID(id uint) (*User, error) {
var user User
if err := r.db.First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
// ... more methods
Key Points: - Define interface first - Use GORM methods for queries - Always check for errors - Return appropriate GORM errors
4. Service Layer (service.go
)¶
Purpose: Implement business logic and orchestration
Example:
package user
import (
"errors"
"golang.org/x/crypto/bcrypt"
)
type UserService struct {
repo UserRepository
}
func NewService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) RegisterUser(req RegisterRequest) (*User, error) {
// Check if user exists
existing, _ := s.repo.FindByEmail(req.Email)
if existing != nil {
return nil, errors.New("email already exists")
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// Create user
user := &User{
Name: req.Name,
Email: req.Email,
PasswordHash: string(hash),
}
if err := s.repo.Create(user); err != nil {
return nil, err
}
return user, nil
}
// ... more methods
Key Points: - Validate business rules - Orchestrate repository calls - Handle errors appropriately - Keep logic testable
5. Handler Layer (handler.go
)¶
Purpose: Handle HTTP requests and responses
Example:
package user
import (
"net/http"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
service *UserService
authService *auth.AuthService
}
func NewHandler(service *UserService, authService *auth.AuthService) *UserHandler {
return &UserHandler{
service: service,
authService: authService,
}
}
// @Summary Register a new user
// @Description Create a new user account
// @Tags auth
// @Accept json
// @Produce json
// @Param user body RegisterRequest true "User registration data"
// @Success 200 {object} TokenResponse
// @Failure 400 {object} ErrorResponse
// @Router /api/v1/auth/register [post]
func (h *UserHandler) Register(c *gin.Context) {
var req RegisterRequest
// Bind and validate request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
// Call service
user, err := h.service.RegisterUser(req)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
// Generate token
token, err := h.authService.GenerateToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to generate token"})
return
}
// Return response
c.JSON(http.StatusOK, TokenResponse{
Token: token,
User: toUserResponse(user),
})
}
Key Points: - Bind request data with validation - Call service layer - Return appropriate HTTP status codes - Add Swagger annotations - Keep handlers thin
👤 How User Management Works¶
Let's trace a registration request through all layers:
Request Flow¶
1. HTTP Request
POST /api/v1/auth/register
Body: {"name": "Alice", "email": "alice@example.com", "password": "secret123"}
↓
2. Handler (handler.go)
- Binds JSON to RegisterRequest
- Validates using binding tags
- Calls service.RegisterUser()
↓
3. Service (service.go)
- Checks if email already exists (calls repo.FindByEmail)
- Hashes password with bcrypt
- Creates User model
- Calls repo.Create()
↓
4. Repository (repository.go)
- Executes GORM Create
- Inserts into database
- Returns user with ID
↓
5. Back to Service
- Returns user to handler
↓
6. Back to Handler
- Generates JWT token
- Converts user to UserResponse
- Returns JSON response
↓
7. HTTP Response
{"token": "eyJ...", "user": {"id": 1, "name": "Alice", ...}}
Authentication Flow¶
Protected endpoints use middleware:
1. Request with Header
Authorization: Bearer eyJhbGc...
↓
2. Auth Middleware (auth/middleware.go)
- Extracts token from header
- Validates JWT signature
- Parses claims (user ID)
- Sets user ID in context
↓
3. Handler
- Gets user ID from context
- Proceeds with business logic
🚀 Adding New Features¶
Follow these steps to add a new feature:
Step-by-Step Checklist¶
- 1. Create domain directory in
internal/
- 2. Define model (
model.go
) - 3. Create migration files
- 4. Define DTOs (
dto.go
) - 5. Create repository interface and implementation (
repository.go
) - 6. Implement business logic (
service.go
) - 7. Create HTTP handlers with Swagger docs (
handler.go
) - 8. Register routes in
router.go
- 9. Update
main.go
for migrations - 10. Write tests
- 11. Update API documentation
🎯 Best Practices¶
1. Error Handling¶
DO:
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("resource not found")
}
return nil, fmt.Errorf("database error: %w", err)
}
DON'T:
2. Validation¶
Always validate at multiple layers: - DTO level - Use binding tags - Service level - Business rules - Database level - Constraints
3. Security¶
Always: - ✅ Hash passwords with bcrypt - ✅ Validate JWT tokens - ✅ Check ownership before operations - ✅ Use parameterized queries (GORM does this) - ✅ Never expose sensitive data in responses
4. Testing¶
Test each layer:
// Repository test (use test database)
func TestCreateTodo(t *testing.T) {
db := setupTestDB()
repo := NewRepository(db)
todo := &Todo{Title: "Test", UserID: 1}
err := repo.Create(todo)
assert.NoError(t, err)
assert.NotZero(t, todo.ID)
}
// Service test (mock repository)
func TestCreateTodo_Service(t *testing.T) {
mockRepo := &MockTodoRepository{}
service := NewService(mockRepo)
mockRepo.On("Create", mock.Anything).Return(nil)
todo, err := service.CreateTodo(1, CreateTodoRequest{Title: "Test"})
assert.NoError(t, err)
assert.NotNil(t, todo)
}
// Handler test (use httptest)
func TestCreateTodoHandler(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
handler := NewHandler(mockService)
router.POST("/todos", handler.CreateTodo)
req := httptest.NewRequest("POST", "/todos", body)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusCreated, rec.Code)
}
5. Logging¶
Add structured logging:
import "log"
func (s *TodoService) CreateTodo(userID uint, req CreateTodoRequest) (*Todo, error) {
log.Printf("Creating todo for user %d: %s", userID, req.Title)
// ... business logic ...
log.Printf("Todo created successfully: %d", todo.ID)
return todo, nil
}
🔄 Common Patterns¶
Pagination¶
type PaginationParams struct {
Page int `form:"page" binding:"min=1"`
Limit int `form:"limit" binding:"min=1,max=100"`
}
func (r *GormTodoRepository) FindByUserIDPaginated(userID uint, params PaginationParams) ([]Todo, int64, error) {
var todos []Todo
var total int64
offset := (params.Page - 1) * params.Limit
// Count total
r.db.Model(&Todo{}).Where("user_id = ?", userID).Count(&total)
// Get paginated results
err := r.db.Where("user_id = ?", userID).
Offset(offset).
Limit(params.Limit).
Order("created_at DESC").
Find(&todos).Error
return todos, total, err
}
Filtering¶
type TodoFilter struct {
Completed *bool `form:"completed"`
Search string `form:"search"`
}
func (r *GormTodoRepository) FindWithFilter(userID uint, filter TodoFilter) ([]Todo, error) {
query := r.db.Where("user_id = ?", userID)
if filter.Completed != nil {
query = query.Where("completed = ?", *filter.Completed)
}
if filter.Search != "" {
query = query.Where("title ILIKE ?", "%"+filter.Search+"%")
}
var todos []Todo
err := query.Find(&todos).Error
return todos, err
}
Batch Operations¶
func (s *TodoService) MarkAllCompleted(userID uint) error {
return s.repo.db.
Model(&Todo{}).
Where("user_id = ? AND completed = ?", userID, false).
Update("completed", true).
Error
}
Transactions¶
func (s *TodoService) BulkCreate(userID uint, todos []CreateTodoRequest) error {
return s.repo.db.Transaction(func(tx *gorm.DB) error {
for _, req := range todos {
todo := &Todo{
Title: req.Title,
UserID: userID,
}
if err := tx.Create(todo).Error; err != nil {
return err // Rollback
}
}
return nil // Commit
})
}
📚 Additional Resources¶
GORM Documentation¶
Gin Documentation¶
Swagger¶
Testing¶
🆘 Getting Help¶
If you're stuck:
- Check the example implementations in
internal/user/
- Review this guide
- Check GORM/Gin documentation
- Look at the tests in
tests/
- Open an issue on GitHub
✅ Checklist for New Features¶
Before considering your feature complete:
- Model defined with proper GORM tags
- Migration files created (up and down)
- DTOs defined with validation tags
- Repository interface and implementation
- Service with business logic
- Handlers with Swagger annotations
- Routes registered in router
- Migration added to main.go
- Tests written for all layers
- Swagger docs regenerated
- API tested with curl/Postman
- Error cases handled
- Logging added
- Documentation updated
Happy Coding! 🚀
Remember: Start simple, test often, and refactor as needed. The architecture supports growth and change!