| name | backend-dev-guidelines |
| description | Comprehensive backend development guide for Go/Chi/PostgreSQL with Clean Architecture. Use when creating routes, handlers, services, interfaces, middleware, working with Chi APIs, sqlc database access, JWT authentication, request validation, correlation IDs, or async patterns. Covers layered architecture (routes → handlers → services → database), interface-first development, error handling, observability, testing strategies, and service decomposition patterns. |
Backend Development Guidelines
Purpose
Establish consistency and best practices for ActionPhase backend development using Go, Chi router, PostgreSQL with sqlc, and Clean Architecture principles.
When to Use This Skill
Automatically activates when working on:
- Creating or modifying routes, endpoints, APIs
- Building handlers, services, interfaces
- Implementing middleware (auth, CORS, recovery, correlation IDs)
- Database operations with sqlc
- JWT authentication and refresh tokens
- Input validation and error handling
- Observability (structured logging, correlation IDs)
- Backend testing and refactoring
- Service decomposition
Quick Start
New Backend Feature Checklist
- Migration: Database schema changes (if needed)
- SQL Queries: Write queries in
queries/*.sql - Code Generation: Run
just sqlgen - Interface: Define in
core/interfaces.go - Tests: Write unit tests first (TDD)
- Service: Implement business logic
- Handler: HTTP request handling
- API Tests: Test endpoints with curl
- Documentation: Update API docs
New Service Checklist
- Interface definition in
core/interfaces.go - Compile-time verification:
var _ Interface = (*Implementation)(nil) - Constructor function with dependencies
- Unit tests with mocks
- Error handling with correlation IDs
- Structured logging
- Input validation
Architecture Overview
Layered Architecture
HTTP Request
↓
Middleware (correlation ID, auth, CORS, recovery)
↓
Routes (Chi router)
↓
Handlers (request binding, validation)
↓
Services (business logic)
↓
sqlc Queries (type-safe SQL)
↓
PostgreSQL
Key Principle: Each layer has ONE responsibility.
See: .claude/context/ARCHITECTURE.md for complete details.
Directory Structure
backend/
├── cmd/server/ # Application entry point
├── pkg/
│ ├── core/ # Domain models, interfaces, errors
│ │ ├── interfaces.go # ALL service interfaces
│ │ ├── models.go # Business entities
│ │ └── errors.go # Typed errors
│ ├── db/
│ │ ├── queries/ # SQL files (*.sql)
│ │ ├── models/ # Generated by sqlc
│ │ ├── migrations/ # Schema migrations
│ │ └── services/ # Service implementations
│ │ ├── phases/ # Decomposed phase service
│ │ ├── actions/ # Decomposed action service
│ │ ├── messages/ # Decomposed message service
│ │ └── *.go # Other services
│ ├── http/
│ │ ├── root.go # Routing + middleware
│ │ ├── middleware/ # Custom middleware
│ │ └── */api.go # HTTP handlers
│ └── util/ # Utilities
├── .env # Environment variables
└── justfile # Development commands
Naming Conventions:
- Packages:
lowercase-services,middleware - Files:
snake_case.go-user_service.go,auth_api.go - Types:
PascalCase-GameService,UserHandler - Functions:
PascalCase(exported),camelCase(private) - Interfaces:
PascalCase + Interface-GameServiceInterface
Core Principles (8 Key Rules)
1. Interfaces First, Implementation Second
ALL service interfaces MUST be defined in backend/pkg/core/interfaces.go
// ✅ ALWAYS: Define interface first
type GameServiceInterface interface {
CreateGame(ctx context.Context, req *CreateGameRequest) (*Game, error)
GetGame(ctx context.Context, id int) (*Game, error)
}
// ✅ ALWAYS: Compile-time verification
var _ GameServiceInterface = (*GameService)(nil)
type GameService struct {
DB *pgxpool.Pool
}
func (s *GameService) CreateGame(ctx context.Context, req *CreateGameRequest) (*Game, error) {
// Implementation
}
Benefits: Easy mocking, clear contracts, compile-time safety, dependency injection.
2. Use sqlc for Type-Safe SQL
// ❌ NEVER: Raw SQL with manual mapping
row := db.QueryRow("SELECT id, title FROM games WHERE id = $1", id)
var game Game
row.Scan(&game.ID, &game.Title)
// ✅ ALWAYS: sqlc-generated queries
queries := db.New(pool)
game, err := queries.GetGame(ctx, id)
Workflow: Write SQL → just sqlgen → Use generated code
3. Handlers Only Handle HTTP, Services Contain Logic
// ❌ NEVER: Business logic in handlers
func (h *Handler) CreateGame(w http.ResponseWriter, r *http.Request) {
// 200 lines of validation, business logic, database calls
}
// ✅ ALWAYS: Delegate to service layer
func (h *Handler) CreateGame(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
correlationID := middleware.GetCorrelationID(ctx)
var req core.CreateGameRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
core.WriteError(w, core.ErrInvalidRequest(err, correlationID))
return
}
game, err := h.service.CreateGame(ctx, &req)
if err != nil {
core.WriteError(w, err)
return
}
core.WriteJSON(w, http.StatusCreated, game)
}
4. Always Use Correlation IDs
// Generate in middleware
correlationID := uuid.New().String()
ctx = context.WithValue(ctx, middleware.CorrelationIDKey, correlationID)
// Use in logging
log.Info().
Str("correlation_id", correlationID).
Str("user_id", userID).
Msg("Game created")
// Include in errors
return core.ErrNotFound("game", gameID, correlationID)
5. Structured Logging with Context
// ✅ ALWAYS: Use zerolog with structured fields
log.Info().
Str("correlation_id", correlationID).
Str("user_id", userID).
Int("game_id", gameID).
Str("action", "create_game").
Msg("Game created successfully")
// ❌ NEVER: fmt.Println or log.Println
fmt.Println("Game created:", gameID)
6. Validate All Input at Handler Layer
// Validate before passing to service
if req.Title == "" {
return core.ErrInvalidRequest(
errors.New("title is required"),
correlationID,
)
}
if len(req.Title) > 255 {
return core.ErrInvalidRequest(
errors.New("title too long"),
correlationID,
)
}
7. Use Typed Errors with Context
// Define in core/errors.go
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
CorrelationID string `json:"correlation_id,omitempty"`
HTTPStatus int `json:"-"`
}
// Usage
if game == nil {
return nil, core.ErrNotFound("game", gameID, correlationID)
}
8. Test-Driven Development (TDD)
// Write test first (should fail)
func TestCreateGame(t *testing.T) {
service := &GameService{DB: mockDB}
game, err := service.CreateGame(ctx, req)
require.NoError(t, err)
assert.Equal(t, "Test Game", game.Title)
}
// Then implement
// Then verify test passes
Common Imports
// HTTP and routing
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
// Database
import (
"github.com/jackc/pgx/v5/pgxpool"
"actionphase/backend/pkg/db"
)
// Logging
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
// JWT
import (
"github.com/golang-jwt/jwt/v5"
)
// Testing
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Context and errors
import (
"context"
"errors"
)
Quick Reference
HTTP Status Codes
| Code | Use Case |
|---|---|
| 200 | Success |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 500 | Server Error |
justfile Commands
just dev # Start server with .env
just sqlgen # Generate Go from SQL
just test # Run all tests
just test-mocks # Fast unit tests (~300ms)
just migrate # Apply migrations
just make_migration # Create new migration
Database Name
CRITICAL: Database name is actionphase, NOT database
postgres://postgres:example@localhost:5432/actionphase
Anti-Patterns to Avoid
❌ Business logic in handlers ❌ Raw SQL without sqlc ❌ Missing correlation IDs ❌ No error handling ❌ No input validation ❌ fmt.Println instead of structured logging ❌ Direct process.env in code (use config) ❌ Forgetting interface definitions ❌ Skipping tests
Service Decomposition Pattern
When to Decompose
Decompose a service when:
- File exceeds 500 lines
- Multiple distinct responsibilities emerge
- Testing becomes difficult
- Code navigation is hard
How to Decompose
Example: Phase service decomposition (1056 lines → 6 files)
services/phases.go (1056 lines)
↓
services/phases/
├── service.go # Main service struct + constructor
├── crud.go # Create, Read, Update, Delete
├── transitions.go # State transitions
├── validation.go # Validation logic
├── history.go # History tracking
└── converters.go # Model conversions
Pattern:
- Create package directory:
services/phases/ - Move service struct to
service.go - Group related methods into focused files
- Update imports across codebase
- Run tests to verify
See: .claude/planning/REFACTOR_00_MASTER_PLAN.md
Navigation Guide
| Need to... | Read this |
|---|---|
| Understand architecture | .claude/context/ARCHITECTURE.md |
| See complete patterns | .claude/reference/BACKEND_ARCHITECTURE.md |
| Handle errors properly | .claude/reference/ERROR_HANDLING.md |
| Implement logging | .claude/reference/LOGGING_STANDARDS.md |
| Document APIs | .claude/reference/API_DOCUMENTATION.md |
| Write tests | .claude/context/TESTING.md |
| Use test fixtures | .claude/context/TEST_DATA.md |
Key Files Reference
Must Read Before Coding:
backend/pkg/core/interfaces.go- ALL service contractsbackend/pkg/core/models.go- Business entitiesbackend/pkg/core/errors.go- Error typesbackend/pkg/http/root.go- Routing + middleware
Implementation Examples:
backend/pkg/db/services/games.go- Simple servicebackend/pkg/db/services/phases/- Decomposed servicebackend/pkg/db/queries/games.sql- sqlc patterns
Testing Requirements
MANDATORY: Tests for all new features and bug fixes
Test Types
Unit Tests (FAST - run first)
- Mock dependencies using interfaces
- Test business logic in isolation
- Run:
just test-mocks(~300ms)
Integration Tests (with database)
- Test with real PostgreSQL
- Use test fixtures
- Run:
SKIP_DB_TESTS=false just test
API Tests (curl verification)
- Verify endpoints return correct data
- Test before E2E tests
- Pattern:
./backend/scripts/api-test.sh
E2E Tests (LAST)
- Only after unit + API + component tests pass
- See:
.claude/context/TESTING.md
Bug Fix Process
- Write test that reproduces bug (should fail)
- Fix the bug
- Verify test passes
- Commit test and fix together
See: .claude/context/TESTING.md for complete testing guide
Authentication Pattern
JWT Access Tokens (15 min) + Refresh Tokens (7 days)
- Access tokens for API requests (in Authorization header)
- Refresh tokens stored in database sessions
- User ID NOT in JWT - fetched from
/api/v1/auth/me - Automatic refresh via frontend interceptors
Security: JWT only contains sub (username), exp, iat, jti
See: /docs/adrs/003-authentication-strategy.md
Related Context Files
.claude/context/ARCHITECTURE.md- Complete architecture patterns.claude/context/TESTING.md- Testing philosophy and patterns.claude/context/TEST_DATA.md- Test fixtures overview.claude/reference/BACKEND_ARCHITECTURE.md- Detailed implementation guide.claude/reference/ERROR_HANDLING.md- Error patterns.claude/reference/LOGGING_STANDARDS.md- Logging best practices.claude/reference/API_DOCUMENTATION.md- API endpoint docs
ADR References
- ADR-001: Technology Stack Selection (Go, Chi, PostgreSQL, sqlc)
- ADR-002: Database Design (Hybrid relational-document with JSONB)
- ADR-003: Authentication Strategy (JWT + Refresh Tokens)
- ADR-004: API Design Principles (RESTful, versioned)
- ADR-006: Observability Approach (Structured logging, correlation IDs)
- ADR-007: Testing Strategy (Test pyramid, TDD)
Location: /docs/adrs/
Skill Status: COMPLETE ✅ Line Count: < 500 ✅ Tech Stack: Go, Chi, PostgreSQL, sqlc ✅ Progressive Disclosure: Links to detailed context files ✅