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
- Code of Conduct
- Getting Started
- Development Workflow
- Code Standards
- Testing
- Documentation
- Pull Requests
- Code Review Process
- Commit Guidelines
- Project Structure
- Common Tasks
- Getting Help
Code of Conduct
Our Principles
- Stability over Velocity: Every change improves robustness (tests, types, lint, docs)
- Unidirectional Flow: Need → Design → Implementation → Test → Doc → PR → Review → Merge
- Full Traceability: All decisions = ADR (Architecture Decision Record), atomic commits with Conventional Commits
- Integrated Security: No secret leaks, mandatory secret scanners, verified dependencies
- 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
-
Fork and Clone
# Fork the repository on GitHub git clone https://github.com/your-username/veza-backend-api.git cd veza-backend-api -
Add Upstream Remote
git remote add upstream https://github.com/veza/veza-backend-api.git -
Install Dependencies
make deps # or manually go mod download go mod tidy -
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 -
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 deployabledevelop: 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 featuresfix/: Bug fixesdocs/: Documentation changesrefactor/: Code refactoringtest/: Test additions or changeschore/: Maintenance tasksperf/: Performance improvements
Examples:
feat/auth-refresh-tokensfix/jwt-uuid-mismatchrefactor/user-service-cleanupdocs/api-documentation-update
Code Standards
Go Code Style
Formatting
- Use
gofmt(automatically applied) - Use
goimportsfor import organization - Run
make formatbefore 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.Errorfwith%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.Contextas 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
- Unused imports: Use
goimportsto clean up - Unused variables: Remove or use
_to explicitly ignore - Shadowed variables: Use different names
- Error handling: Always check and handle errors
- 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 projectspkg/: Public library code, importable by other projectscmd/: 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
-
Unit Tests: Test individual functions/methods
go test ./internal/services/... -
Integration Tests: Test with real dependencies
go test -tags=integration ./tests/integration/... -
Race Detection: Detect race conditions
go test -race ./...
Test Best Practices
- Use table-driven tests for multiple scenarios
- Use
testify/assertandtestify/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
-
Update your branch
git checkout main git pull upstream main git checkout feat/your-feature git rebase main -
Run quality checks
make format make lint make vet make test -
Verify your changes
- All tests pass
- Code compiles without errors
- No linting errors
- Documentation updated
Creating a PR
-
Push your branch
git push origin feat/your-feature -
Create PR on GitHub
- Use the PR template
- Fill in all sections
- Link related issues
- Add reviewers
-
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
- Functionality: Does it work as intended?
- Code Quality: Follows style guidelines?
- Tests: Adequate test coverage?
- Documentation: Properly documented?
- Performance: No obvious performance issues?
- 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 featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code refactoringtest: Test additions/changeschore: Maintenance tasksperf: Performance improvementsci: 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
-
Define DTOs (
internal/dto/)type CreateUserRequest struct { Email string `json:"email" binding:"required,email"` Username string `json:"username" binding:"required,min=3"` } -
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 } // ... } -
Add Service Logic (
internal/services/)func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) { // Business logic } -
Register Route (
cmd/modern-server/main.goor router)api.POST("/users", userHandler.CreateUser) -
Add Tests
func TestUserHandler_CreateUser(t *testing.T) { // Test implementation } -
Update Documentation
- Update Swagger annotations
- Update API documentation
Adding a New Service
-
Define Interface (
internal/services/)type ProductService interface { CreateProduct(ctx context.Context, req *CreateProductRequest) (*Product, error) GetProduct(ctx context.Context, id uuid.UUID) (*Product, error) } -
Implement Service
type productService struct { repo ProductRepository } func NewProductService(repo ProductRepository) ProductService { return &productService{repo: repo} } -
Add Tests
func TestProductService_CreateProduct(t *testing.T) { // Test implementation }
Adding Database Migrations
-
Create Migration File
migrate create -ext sql -dir migrations -seq add_products_table -
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 ); -
Write Rollback (
migrations/XXXXXX_add_products_table.down.sql)DROP TABLE products; -
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.htmlwhen 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:
- What you're trying to do
- What you've tried
- Error messages (if any)
- Relevant code snippets
- 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! 🚀