veza/veza-backend-api/CONTRIBUTING.md
2025-12-25 11:06:54 +01:00

19 KiB

Contributing to Veza Backend API

Thank you for your interest in contributing to Veza Backend API! This guide will help you get started and ensure your contributions align with our standards.

Table of Contents

  1. Code of Conduct
  2. Getting Started
  3. Development Workflow
  4. Code Standards
  5. Testing
  6. Documentation
  7. Pull Requests
  8. Code Review Process
  9. Commit Guidelines
  10. Project Structure
  11. Common Tasks
  12. Getting Help

Code of Conduct

Our Principles

  1. Stability over Velocity: Every change improves robustness (tests, types, lint, docs)
  2. Unidirectional Flow: Need → Design → Implementation → Test → Doc → PR → Review → Merge
  3. Full Traceability: All decisions = ADR (Architecture Decision Record), atomic commits with Conventional Commits
  4. Integrated Security: No secret leaks, mandatory secret scanners, verified dependencies
  5. Controlled Autonomy: AI agents work in background, always via PR + checklists, mandatory human review

Expected Behavior

  • Be respectful and inclusive
  • Welcome newcomers and help them learn
  • Focus on constructive feedback
  • Respect different viewpoints and experiences

Unacceptable Behavior

  • Harassment or discriminatory language
  • Personal attacks
  • Trolling or inflammatory comments
  • Publishing others' private information

Getting Started

Prerequisites

  • Go 1.23 or higher
  • PostgreSQL 12+ (for database)
  • Redis (optional, for caching and rate limiting)
  • Docker (optional, for containerized development)
  • Git 2.30+

Initial Setup

  1. Fork and Clone

    # Fork the repository on GitHub
    git clone https://github.com/your-username/veza-backend-api.git
    cd veza-backend-api
    
  2. Add Upstream Remote

    git remote add upstream https://github.com/veza/veza-backend-api.git
    
  3. Install Dependencies

    make deps
    # or manually
    go mod download
    go mod tidy
    
  4. Set Up Environment

    # Copy example environment file
    cp .env.example .env
    
    # Edit .env with your configuration
    # Minimum required:
    # - DATABASE_URL
    # - JWT_SECRET (min 32 characters)
    # - APP_ENV
    
  5. Verify Installation

    # Build the application
    make build
    
    # Run tests
    make test
    
    # Check code quality
    make lint
    make vet
    

Development Workflow

Branching Model

We follow a feature branch workflow:

  • main: Always stable, always deployable
  • develop: Integration branch (optional)
  • Feature branches: feat/<feature-name>
  • Bug fix branches: fix/<bug-description>
  • Documentation branches: docs/<description>
  • Refactoring branches: refactor/<description>

Creating a Branch

# Update main branch
git checkout main
git pull upstream main

# Create feature branch
git checkout -b feat/your-feature-name

# Or use the shorthand
git checkout -b feat/your-feature-name upstream/main

Branch Naming Conventions

  • feat/: New features
  • fix/: Bug fixes
  • docs/: Documentation changes
  • refactor/: Code refactoring
  • test/: Test additions or changes
  • chore/: Maintenance tasks
  • perf/: Performance improvements

Examples:

  • feat/auth-refresh-tokens
  • fix/jwt-uuid-mismatch
  • refactor/user-service-cleanup
  • docs/api-documentation-update

Code Standards

Go Code Style

Formatting

  • Use gofmt (automatically applied)
  • Use goimports for import organization
  • Run make format before committing
# Format code
make format

# Or manually
go fmt ./...
goimports -w .

Naming Conventions

  • Packages: lowercase, single word, no underscores
  • Exported functions/types: PascalCase
  • Unexported functions/types: camelCase
  • Constants: PascalCase for exported, camelCase for unexported
  • Interfaces: PascalCase, often end with -er (e.g., Reader, Writer)
// Good
package user

type UserService struct {
    repo UserRepository
}

func (s *UserService) GetByID(id uuid.UUID) (*User, error) {
    // ...
}

// Bad
package user_service

type user_service struct {
    repo user_repository
}

func (s *user_service) get_by_id(id uuid.UUID) (*user, error) {
    // ...
}

Code Organization

  • Keep functions small and focused (single responsibility)
  • Prefer composition over inheritance
  • Use interfaces for abstraction
  • Avoid deep nesting (max 3-4 levels)
  • Return errors explicitly, don't ignore them
// Good
func (s *UserService) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
    if id == uuid.Nil {
        return nil, errors.New("user ID cannot be nil")
    }
    
    user, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("failed to get user: %w", err)
    }
    
    return user, nil
}

// Bad
func (s *UserService) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
    user, _ := s.repo.FindByID(ctx, id) // Error ignored!
    return user, nil
}

Error Handling

  • Always handle errors explicitly
  • Wrap errors with context using fmt.Errorf with %w
  • Use sentinel errors for expected error conditions
  • Return errors, don't log them (let the caller decide)
// Good
var ErrUserNotFound = errors.New("user not found")

func (s *UserService) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
    user, err := s.repo.FindByID(ctx, id)
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            return nil, ErrUserNotFound
        }
        return nil, fmt.Errorf("failed to get user %s: %w", id, err)
    }
    return user, nil
}

// Bad
func (s *UserService) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
    user, err := s.repo.FindByID(ctx, id)
    if err != nil {
        log.Printf("Error: %v", err) // Don't log here!
        return nil, err
    }
    return user, nil
}

Context Usage

  • Always accept context.Context as the first parameter
  • Use context for cancellation, timeouts, and request-scoped values
  • Don't store contexts in structs
// Good
func (s *UserService) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    
    user, err := s.repo.FindByID(ctx, id)
    // ...
}

// Bad
type UserService struct {
    ctx context.Context // Don't store context!
}

Comments and Documentation

  • Document all exported functions, types, and constants
  • Use complete sentences
  • Start with the name of the thing being described
  • Use // for comments, /* */ for block comments (rarely)
// UserService provides operations for managing users.
type UserService struct {
    repo UserRepository
}

// GetByID retrieves a user by their unique identifier.
// It returns ErrUserNotFound if the user does not exist.
func (s *UserService) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
    // ...
}

Linting and Code Quality

Required Tools

  • golangci-lint: Comprehensive Go linter
  • go vet: Built-in Go static analysis
  • goimports: Import organization
# Install golangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Run linters
make lint
make vet

Common Issues to Avoid

  1. Unused imports: Use goimports to clean up
  2. Unused variables: Remove or use _ to explicitly ignore
  3. Shadowed variables: Use different names
  4. Error handling: Always check and handle errors
  5. Race conditions: Use proper synchronization

Project Structure

veza-backend-api/
├── cmd/                    # Application entry points
│   ├── modern-server/      # Main server
│   └── tools/              # Utility tools
├── internal/               # Private application code
│   ├── handlers/          # HTTP handlers
│   ├── services/           # Business logic
│   ├── repositories/       # Data access layer
│   ├── models/             # Data models
│   ├── dto/                # Data transfer objects
│   ├── middleware/         # HTTP middleware
│   ├── validators/          # Input validation
│   ├── errors/              # Error definitions
│   └── config/              # Configuration
├── pkg/                    # Public library code
├── migrations/             # Database migrations
├── tests/                  # Integration tests
├── docs/                   # Documentation
├── scripts/                # Utility scripts
└── Makefile               # Build automation

Package Organization

  • internal/: Private code, not importable by other projects
  • pkg/: Public library code, importable by other projects
  • cmd/: Application entry points
  • Keep packages focused and cohesive
  • Avoid circular dependencies

Testing

Test Requirements

  • All new features must include tests
  • Bug fixes must include regression tests
  • Test coverage should be ≥ 80% for new code
  • Tests must pass before submitting PR

Test Structure

package user_test

import (
    "context"
    "testing"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestUserService_GetByID(t *testing.T) {
    // Arrange
    ctx := context.Background()
    service := setupUserService(t)
    user := createTestUser(t, ctx)
    
    // Act
    result, err := service.GetByID(ctx, user.ID)
    
    // Assert
    require.NoError(t, err)
    assert.Equal(t, user.ID, result.ID)
    assert.Equal(t, user.Email, result.Email)
}

Test Types

  1. Unit Tests: Test individual functions/methods

    go test ./internal/services/...
    
  2. Integration Tests: Test with real dependencies

    go test -tags=integration ./tests/integration/...
    
  3. Race Detection: Detect race conditions

    go test -race ./...
    

Test Best Practices

  • Use table-driven tests for multiple scenarios
  • Use testify/assert and testify/require
  • Clean up test data (use t.Cleanup())
  • Use test helpers for common setup
  • Mock external dependencies
func TestUserService_GetByID_TableDriven(t *testing.T) {
    tests := []struct {
        name    string
        userID  uuid.UUID
        wantErr bool
        errType error
    }{
        {
            name:    "valid user",
            userID:  validUserID,
            wantErr: false,
        },
        {
            name:    "user not found",
            userID:  uuid.New(),
            wantErr: true,
            errType: ErrUserNotFound,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Test implementation
        })
    }
}

Running Tests

# Run all tests
make test

# Run tests with coverage
make test-coverage

# Run tests with race detection
make test-race

# Run specific test
go test -v ./internal/services/user_service_test.go

# Run tests matching pattern
go test -v -run TestUserService

Documentation

Code Documentation

  • Document all exported functions, types, and constants
  • Use Go doc comments (start with the name)
  • Include examples for complex functions
  • Document error conditions
// User represents a user in the system.
//
// Example:
//   user := &User{
//       ID:    uuid.New(),
//       Email: "user@example.com",
//   }
type User struct {
    ID    uuid.UUID
    Email string
}

// GetByID retrieves a user by ID.
//
// Returns ErrUserNotFound if the user does not exist.
func (s *UserService) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
    // ...
}

API Documentation

  • Update Swagger/OpenAPI documentation for API changes
  • Include request/response examples
  • Document error responses
  • Document authentication requirements

README Updates

  • Update README for significant changes
  • Document new environment variables
  • Update installation instructions if needed
  • Add migration notes for breaking changes

Pull Requests

Before Creating a PR

  1. Update your branch

    git checkout main
    git pull upstream main
    git checkout feat/your-feature
    git rebase main
    
  2. Run quality checks

    make format
    make lint
    make vet
    make test
    
  3. Verify your changes

    • All tests pass
    • Code compiles without errors
    • No linting errors
    • Documentation updated

Creating a PR

  1. Push your branch

    git push origin feat/your-feature
    
  2. Create PR on GitHub

    • Use the PR template
    • Fill in all sections
    • Link related issues
    • Add reviewers
  3. PR Title Format

    [CATEGORY] Brief description
    
    Examples:
    [FEAT] Add user profile endpoint
    [FIX] Correct JWT token validation
    [DOCS] Update API documentation
    

PR Description Template

## Description
Brief description of changes

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Related Issues
Closes #123

## Changes Made
- Change 1
- Change 2

## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing performed

## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Comments added for complex code
- [ ] Documentation updated
- [ ] No new warnings generated
- [ ] Tests added and passing
- [ ] Breaking changes documented

PR Best Practices

  • Keep PRs focused and small (< 500 lines ideally)
  • One logical change per PR
  • Include tests for new features
  • Update documentation
  • Respond to review comments promptly
  • Rebase on main if conflicts arise

Code Review Process

Review Criteria

  1. Functionality: Does it work as intended?
  2. Code Quality: Follows style guidelines?
  3. Tests: Adequate test coverage?
  4. Documentation: Properly documented?
  5. Performance: No obvious performance issues?
  6. Security: No security vulnerabilities?

Review Guidelines

  • Be constructive and respectful
  • Focus on code, not the person
  • Explain the "why" behind suggestions
  • Approve when satisfied
  • Request changes when needed

Responding to Reviews

  • Address all comments
  • Ask for clarification if needed
  • Make requested changes
  • Re-request review when ready
  • Thank reviewers for their time

Commit Guidelines

Conventional Commits

We follow Conventional Commits specification:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Commit Types

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • style: Code style changes (formatting, etc.)
  • refactor: Code refactoring
  • test: Test additions/changes
  • chore: Maintenance tasks
  • perf: Performance improvements
  • ci: CI/CD changes

Commit Examples

feat(auth): add refresh token endpoint

fix(user): correct email validation regex

docs(api): update authentication documentation

refactor(service): simplify user service logic

test(user): add tests for user creation

Commit Best Practices

  • Write clear, descriptive commit messages
  • Use present tense ("add" not "added")
  • Keep commits atomic (one logical change)
  • Reference issues in commit message
  • Don't commit generated files
# Good
feat(auth): add refresh token endpoint

Implements refresh token generation and validation.
Adds /api/v1/auth/refresh endpoint.

Closes #123

# Bad
fix stuff

Common Tasks

Adding a New Endpoint

  1. Define DTOs (internal/dto/)

    type CreateUserRequest struct {
        Email    string `json:"email" binding:"required,email"`
        Username string `json:"username" binding:"required,min=3"`
    }
    
  2. Create Handler (internal/handlers/)

    func (h *UserHandler) CreateUser(c *gin.Context) {
        var req CreateUserRequest
        if err := c.ShouldBindJSON(&req); err != nil {
            RespondWithError(c, http.StatusBadRequest, err)
            return
        }
        // ...
    }
    
  3. Add Service Logic (internal/services/)

    func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) {
        // Business logic
    }
    
  4. Register Route (cmd/modern-server/main.go or router)

    api.POST("/users", userHandler.CreateUser)
    
  5. Add Tests

    func TestUserHandler_CreateUser(t *testing.T) {
        // Test implementation
    }
    
  6. Update Documentation

    • Update Swagger annotations
    • Update API documentation

Adding a New Service

  1. Define Interface (internal/services/)

    type ProductService interface {
        CreateProduct(ctx context.Context, req *CreateProductRequest) (*Product, error)
        GetProduct(ctx context.Context, id uuid.UUID) (*Product, error)
    }
    
  2. Implement Service

    type productService struct {
        repo ProductRepository
    }
    
    func NewProductService(repo ProductRepository) ProductService {
        return &productService{repo: repo}
    }
    
  3. Add Tests

    func TestProductService_CreateProduct(t *testing.T) {
        // Test implementation
    }
    

Adding Database Migrations

  1. Create Migration File

    migrate create -ext sql -dir migrations -seq add_products_table
    
  2. Write Migration (migrations/XXXXXX_add_products_table.up.sql)

    CREATE TABLE products (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        name VARCHAR(255) NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
  3. Write Rollback (migrations/XXXXXX_add_products_table.down.sql)

    DROP TABLE products;
    
  4. Test Migration

    migrate -path ./migrations -database "$DATABASE_URL" up
    migrate -path ./migrations -database "$DATABASE_URL" down
    

Getting Help

Resources

  • Documentation: See docs/ directory
  • API Documentation: /swagger/index.html when server is running
  • Architecture Guide: docs/ARCHITECTURE.md
  • Development Guide: docs/DEVELOPMENT_SETUP_GUIDE.md
  • Troubleshooting: docs/TROUBLESHOOTING_GUIDE.md

Communication Channels

  • GitHub Issues: For bug reports and feature requests
  • GitHub Discussions: For questions and discussions
  • Pull Requests: For code-related questions

Asking Questions

When asking for help, include:

  1. What you're trying to do
  2. What you've tried
  3. Error messages (if any)
  4. Relevant code snippets
  5. Environment details (OS, Go version, etc.)

Reporting Bugs

Use the bug report template:

## Description
Brief description of the bug

## Steps to Reproduce
1. Step 1
2. Step 2
3. Step 3

## Expected Behavior
What should happen

## Actual Behavior
What actually happens

## Environment
- OS: [e.g., Linux, macOS, Windows]
- Go Version: [e.g., 1.23.0]
- Application Version: [e.g., 1.2.0]

## Additional Context
Any other relevant information

Recognition

Contributors are recognized in:

  • README: Contributors section
  • CHANGELOG: Release notes
  • GitHub: Contributors page

Thank you for contributing to Veza Backend API! 🚀