# Coding Guidelines ## Tech Stack | Component | Required | Forbidden | |-----------|----------|-----------| | HTTP Router | chi/v5 | gin, echo, fiber | | Database | sqlx | GORM, raw sql | | Logging | slog | log, logrus, zap | | Config | Viper + env vars | os.Getenv directly | | Testing | testing + testify | ginkgo, gomega | | Kubernetes | client-go | kubectl subprocess | ## File Structure ``` cmd/{service}/ # Entry points internal/ # Private code (hexagonal) ├── domain/ # Pure models, no deps ├── port/ # Interfaces only ├── service/ # Business logic ├── handlers/ # HTTP handlers ├── adapter/ # Infrastructure ├── auth/ # Authentication ├── middleware/ # HTTP middleware ├── worker/ # Background jobs └── webhook/ # Event dispatch pkg/ # Public packages ``` ## Handler Pattern All handlers implement the `Mount` interface and use `pkg/api` for responses: ```go type UsersHandler struct { service *service.UserService } func (h *UsersHandler) Mount(r chi.Router) { r.Route("/users", func(r chi.Router) { r.Get("/", h.List) r.Post("/", h.Create) r.Get("/{id}", h.Get) }) } func (h *UsersHandler) Create(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { api.BadRequest(w, "invalid request body") return } user, err := h.service.Create(r.Context(), req) if err != nil { api.Error(w, err) return } api.Created(w, user) } ``` ## Response Helpers (`pkg/api`) ```go api.OK(w, data) // 200 with JSON body api.Created(w, data) // 201 with JSON body api.NoContent(w) // 204 api.BadRequest(w, msg) // 400 api.Unauthorized(w, msg) // 401 api.Forbidden(w, msg) // 403 api.NotFound(w, msg) // 404 api.Conflict(w, msg) // 409 api.Error(w, err) // 500 (or mapped status) ``` ## Port/Adapter Pattern Domain interfaces in `internal/port/`: ```go // port/project.go type ProjectRepository interface { List(ctx context.Context) ([]domain.Project, error) Get(ctx context.Context, id string) (*domain.Project, error) } type CommandExecutor interface { Execute(ctx context.Context, project string, cmd domain.Command) (*domain.CommandResult, error) } ``` Implementations in `internal/adapter/{name}/`: ```go // adapter/kubernetes/repository.go type Repository struct { client kubernetes.Interface } func (r *Repository) List(ctx context.Context) ([]domain.Project, error) { // Implementation using client-go } ``` ## Domain Models Pure structs in `internal/domain/`, NO external dependencies: ```go // domain/project.go package domain type Project struct { ID string Name string Status ProjectStatus Workspace string } type ProjectStatus string const ( StatusRunning ProjectStatus = "running" StatusPending ProjectStatus = "pending" StatusFailed ProjectStatus = "failed" ) ``` ## Error Handling Use wrapped errors with context: ```go if err != nil { return fmt.Errorf("create project %s: %w", name, err) } ``` Define domain errors in `internal/domain/errors.go`: ```go var ( ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ErrConflict = errors.New("already exists") ) ``` ## Testing - Unit tests next to source: `foo_test.go` - Table-driven tests preferred - Mocks implement port interfaces - Test files in `testutil/` for shared helpers ```go func TestHandler_Create(t *testing.T) { tests := []struct { name string input CreateRequest wantStatus int }{ {"valid", CreateRequest{Name: "test"}, 201}, {"empty name", CreateRequest{}, 400}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // ... }) } } ``` ## Naming Conventions - Files: `snake_case.go` - Packages: `lowercase` (no underscores) - Types: `PascalCase` - Functions/Methods: `PascalCase` (exported), `camelCase` (private) - Constants: `PascalCase` or `SCREAMING_SNAKE` for env vars - Interfaces: Don't prefix with `I`, suffix with role (`Repository`, `Service`) ## Size Limits - **500 lines max per file** - split when exceeded - **100 lines max per function** - extract helpers - **5 parameters max per function** - use struct if more