@eltonssouza/development-utility-kit 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/README.md +24 -0
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +46 -0
- package/.claude/agents/stack-resolver.md +104 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/hooks/flow-guard.js +261 -0
- package/.claude/hooks/flow-state.js +197 -0
- package/.claude/local/CLAUDE.md +71 -0
- package/.claude/settings.json +55 -0
- package/.claude/skills/README.md +331 -0
- package/.claude/skills/active-project/SKILL.md +131 -0
- package/.claude/skills/api-integration-test/SKILL.md +84 -0
- package/.claude/skills/auto-test-guard/SKILL.md +239 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +62 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +189 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +80 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +86 -0
- package/.claude/skills/project-manager/SKILL.md +334 -0
- package/.claude/skills/quality-standards/SKILL.md +203 -0
- package/.claude/skills/quick-feature/SKILL.md +266 -0
- package/.claude/skills/run-sprint/SKILL.md +41 -0
- package/.claude/skills/scaffold/SKILL.md +60 -0
- package/.claude/skills/stack-discovery/SKILL.md +161 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +256 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +97 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/dotnet/aspire-9.md +528 -0
- package/.claude/stacks/go/gin-1.10.md +570 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/node/express-5.md +538 -0
- package/.claude/stacks/python/django-5.md +483 -0
- package/.claude/stacks/python/fastapi-0.115.md +522 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +472 -0
- package/README.md +412 -0
- package/bin/cli.js +848 -0
- package/bin/lib/adr.js +146 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/doctor.js +527 -0
- package/bin/lib/help.js +328 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/lint-allowlist.json +15 -0
- package/bin/lib/lint.js +798 -0
- package/bin/lib/local-dir.js +68 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +321 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +853 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/cli-reference.en.md +538 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
- package/dashboard/public/content/docs/pipeline.en.md +414 -0
- package/dashboard/public/content/docs/plugins.en.md +289 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +484 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
- package/dashboard/public/content/manifest.json +114 -0
- package/dashboard/public/content/manual/backend.en.md +1053 -0
- package/dashboard/public/content/manual/existing-project.en.md +848 -0
- package/dashboard/public/content/manual/frontend.en.md +1008 -0
- package/dashboard/public/content/manual/fullstack.en.md +1459 -0
- package/dashboard/public/content/manual/mobile.en.md +837 -0
- package/dashboard/public/content/manual/quickstart.en.md +169 -0
- package/dashboard/public/index.html +217 -0
- package/dashboard/public/style.css +857 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +421 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
---
|
|
2
|
+
stack: go/gin-1.10
|
|
3
|
+
versions_covered: "1.10.x — 1.12.x"
|
|
4
|
+
last_validated: 2026-05-28
|
|
5
|
+
validated_against: "reference pack — Go 1.23 + Gin 1.10 + GORM 1.25 + golang-migrate 4.18"
|
|
6
|
+
status: active
|
|
7
|
+
pack_owner: "@elton"
|
|
8
|
+
security_review: 2026-05-28
|
|
9
|
+
next_review_due: 2027-05-28
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Go 1.23+ + Gin 1.10+
|
|
13
|
+
|
|
14
|
+
Canonical knowledge pack for Go HTTP services on Gin 1.10+. Gin is the right pick when the service is HTTP-heavy with middleware-friendly routing and modest dependency footprint. For projects targeting standard `net/http` + chi or echo, the patterns transfer with minor mechanical changes; this pack assumes Gin.
|
|
15
|
+
|
|
16
|
+
## 1. When to use this pack
|
|
17
|
+
|
|
18
|
+
- Project declares `Primary stack: Go 1.23+ + Gin 1.10+` in `## Project Identity`.
|
|
19
|
+
- `go.mod` declares `go 1.23` (or newer) and a direct dependency on `github.com/gin-gonic/gin v1.10.x`.
|
|
20
|
+
- Service is API-first (REST), modest throughput target (Gin can handle 50k+ req/s with care; for extreme throughput consider `fasthttp` or hand-rolled `net/http`).
|
|
21
|
+
- For pure CLI / batch jobs without HTTP: do not use this pack — use a plain Go project with cobra/urfave-cli.
|
|
22
|
+
- For full-stack with server-rendered HTML: not Gin's strength; consider `templ` + chi or Buffalo.
|
|
23
|
+
|
|
24
|
+
## 2. Stack baseline (what this pack assumes)
|
|
25
|
+
|
|
26
|
+
| Component | Version range | Notes |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Go | 1.23 (min) / 1.24 (latest stable) | Generics maduros; iterators (range-over-func) na 1.23; toolchain directive in go.mod |
|
|
29
|
+
| Gin | 1.10.x — 1.12.x | Stable middleware API; HTTP/2 native; `gin.Context` is the canonical handler signature |
|
|
30
|
+
| ORM | GORM 1.25.x | OR `sqlc` (typed queries from SQL) for SQL purists |
|
|
31
|
+
| Migrations | golang-migrate 4.18.x | Pure-Go; embedded SQL files via `embed.FS` |
|
|
32
|
+
| Validation | `go-playground/validator` 10.x | Used by Gin's `c.ShouldBindJSON()`; struct tag-driven |
|
|
33
|
+
| Build | `go build` + Makefile | Module mode mandatory; `go mod tidy` in CI |
|
|
34
|
+
| Tests | `testing` stdlib + `testify` 1.10.x | `gomock` 0.5 for mocking; Testcontainers Go 0.34 for integration |
|
|
35
|
+
| Mutation | `go-mutesting` (community) | Target ≥70% on `domain/` + `application/` |
|
|
36
|
+
| Coverage | `go test -coverprofile=cov.out -covermode=atomic` | Target ≥85% lines (`go tool cover -func=cov.out`) |
|
|
37
|
+
| Static analysis | `golangci-lint` v1.61+ (or `v2.0+`) | Bundles 50+ linters; `revive`, `govet`, `staticcheck`, `gosec` |
|
|
38
|
+
| Security scan | `govulncheck` (golang.org/x/vuln) + `gosec` | `govulncheck ./...` checks CVE in code path; `gosec` scans code patterns |
|
|
39
|
+
| Observability | OpenTelemetry SDK + `otelgin` middleware | W3C Trace Context; auto-instruments HTTP, GORM, http.Client |
|
|
40
|
+
| HTTP client | `net/http` with `&http.Client{Timeout: 10*time.Second}` | NEVER `http.Get(url)` directly (no timeout = leaked goroutines) |
|
|
41
|
+
| Config | `kelseyhightower/envconfig` OR `spf13/viper` | env-driven; validated at startup |
|
|
42
|
+
|
|
43
|
+
## 3. Project structure (DDD / hexagonal)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
service/
|
|
47
|
+
├── go.mod
|
|
48
|
+
├── go.sum
|
|
49
|
+
├── Makefile
|
|
50
|
+
├── cmd/
|
|
51
|
+
│ └── server/
|
|
52
|
+
│ └── main.go # bootstrap: cfg → DI graph → start gin
|
|
53
|
+
├── internal/ # not importable by other modules
|
|
54
|
+
│ ├── domain/ # pure Go; no Gin, no GORM
|
|
55
|
+
│ │ ├── product/
|
|
56
|
+
│ │ │ ├── entity.go
|
|
57
|
+
│ │ │ ├── repository.go # interface
|
|
58
|
+
│ │ │ └── service.go # pure domain service
|
|
59
|
+
│ │ └── shared/
|
|
60
|
+
│ ├── application/ # use cases (1 method per type)
|
|
61
|
+
│ │ ├── product/
|
|
62
|
+
│ │ │ ├── create_product.go
|
|
63
|
+
│ │ │ └── list_products.go
|
|
64
|
+
│ │ └── shared/
|
|
65
|
+
│ ├── infrastructure/ # GORM, http.Client, AWS SDK, etc.
|
|
66
|
+
│ │ ├── db/
|
|
67
|
+
│ │ │ ├── gorm.go # *gorm.DB initializer
|
|
68
|
+
│ │ │ └── migration.go # golang-migrate runner
|
|
69
|
+
│ │ ├── product/
|
|
70
|
+
│ │ │ ├── model.go # GORM struct
|
|
71
|
+
│ │ │ └── repository.go # adapter for domain.Repository
|
|
72
|
+
│ │ └── http/
|
|
73
|
+
│ │ └── client.go # shared http.Client
|
|
74
|
+
│ ├── api/ # Gin routes + handlers + DTOs
|
|
75
|
+
│ │ ├── product/
|
|
76
|
+
│ │ │ ├── handler.go
|
|
77
|
+
│ │ │ ├── dto.go # request/response structs
|
|
78
|
+
│ │ │ └── router.go
|
|
79
|
+
│ │ └── shared/
|
|
80
|
+
│ │ ├── error_handler.go # RFC 9457 ProblemDetails
|
|
81
|
+
│ │ └── middleware.go
|
|
82
|
+
│ └── config/
|
|
83
|
+
│ └── config.go # envconfig struct
|
|
84
|
+
├── migrations/ # SQL files for golang-migrate
|
|
85
|
+
│ ├── 000001_create_products.up.sql
|
|
86
|
+
│ └── 000001_create_products.down.sql
|
|
87
|
+
└── tests/
|
|
88
|
+
├── unit/
|
|
89
|
+
├── integration/
|
|
90
|
+
└── e2e/
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Rule**: `internal/` makes the layers un-importable by other modules. `domain/` and `application/` contain **zero Gin and zero GORM imports** — pure Go testable without any infra. `infrastructure/` is the only place importing `gorm.io/...`. `api/` is the only place importing `gin-gonic/gin`.
|
|
94
|
+
|
|
95
|
+
## 4. Code patterns
|
|
96
|
+
|
|
97
|
+
### Domain entity (no GORM, no Gin)
|
|
98
|
+
|
|
99
|
+
```go
|
|
100
|
+
// internal/domain/product/entity.go
|
|
101
|
+
package product
|
|
102
|
+
|
|
103
|
+
import (
|
|
104
|
+
"errors"
|
|
105
|
+
"time"
|
|
106
|
+
"github.com/google/uuid"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
type Product struct {
|
|
110
|
+
ID uuid.UUID
|
|
111
|
+
Name string
|
|
112
|
+
Price int64 // cents — never float for money
|
|
113
|
+
Stock int
|
|
114
|
+
CreatedAt time.Time
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func (p Product) Reserve(qty int) (Product, error) {
|
|
118
|
+
if qty <= 0 {
|
|
119
|
+
return Product{}, errors.New("qty must be positive")
|
|
120
|
+
}
|
|
121
|
+
if qty > p.Stock {
|
|
122
|
+
return Product{}, errors.New("insufficient stock")
|
|
123
|
+
}
|
|
124
|
+
p.Stock -= qty
|
|
125
|
+
return p, nil
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Rule**: money in `int64` cents (or `decimal.Decimal` if shopspring/decimal is acceptable). NEVER `float64` for money — precision loss is real. Domain methods take and return value types; immutability by convention.
|
|
130
|
+
|
|
131
|
+
### Domain port (interface) + infrastructure adapter
|
|
132
|
+
|
|
133
|
+
```go
|
|
134
|
+
// internal/domain/product/repository.go
|
|
135
|
+
package product
|
|
136
|
+
|
|
137
|
+
import (
|
|
138
|
+
"context"
|
|
139
|
+
"github.com/google/uuid"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
type Repository interface {
|
|
143
|
+
Get(ctx context.Context, id uuid.UUID) (*Product, error)
|
|
144
|
+
Save(ctx context.Context, p Product) error
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```go
|
|
149
|
+
// internal/infrastructure/product/model.go
|
|
150
|
+
package product
|
|
151
|
+
|
|
152
|
+
import (
|
|
153
|
+
"time"
|
|
154
|
+
"github.com/google/uuid"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
type ProductGORM struct {
|
|
158
|
+
ID uuid.UUID `gorm:"type:uuid;primaryKey"`
|
|
159
|
+
Name string `gorm:"size:120;index"`
|
|
160
|
+
Price int64
|
|
161
|
+
Stock int
|
|
162
|
+
CreatedAt time.Time
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func (ProductGORM) TableName() string { return "products" }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```go
|
|
169
|
+
// internal/infrastructure/product/repository.go
|
|
170
|
+
package product
|
|
171
|
+
|
|
172
|
+
import (
|
|
173
|
+
"context"
|
|
174
|
+
"errors"
|
|
175
|
+
"github.com/google/uuid"
|
|
176
|
+
"gorm.io/gorm"
|
|
177
|
+
domain "service/internal/domain/product"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
type GormRepository struct {
|
|
181
|
+
db *gorm.DB
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func New(db *gorm.DB) *GormRepository {
|
|
185
|
+
return &GormRepository{db: db}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func (r *GormRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Product, error) {
|
|
189
|
+
var row ProductGORM
|
|
190
|
+
if err := r.db.WithContext(ctx).First(&row, "id = ?", id).Error; err != nil {
|
|
191
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
192
|
+
return nil, nil
|
|
193
|
+
}
|
|
194
|
+
return nil, err
|
|
195
|
+
}
|
|
196
|
+
return toDomain(row), nil
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func (r *GormRepository) Save(ctx context.Context, p domain.Product) error {
|
|
200
|
+
row := ProductGORM{
|
|
201
|
+
ID: p.ID, Name: p.Name, Price: p.Price, Stock: p.Stock, CreatedAt: p.CreatedAt,
|
|
202
|
+
}
|
|
203
|
+
return r.db.WithContext(ctx).Save(&row).Error
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
func toDomain(r ProductGORM) *domain.Product {
|
|
207
|
+
return &domain.Product{
|
|
208
|
+
ID: r.ID, Name: r.Name, Price: r.Price, Stock: r.Stock, CreatedAt: r.CreatedAt,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Rule**: domain `Repository` is an interface in `domain/`. Implementation in `infrastructure/`. Use cases depend on the interface. UUID primary key — never auto-increment for distributed systems.
|
|
214
|
+
|
|
215
|
+
### Gin handler (thin)
|
|
216
|
+
|
|
217
|
+
```go
|
|
218
|
+
// internal/api/product/dto.go
|
|
219
|
+
package product
|
|
220
|
+
|
|
221
|
+
import "time"
|
|
222
|
+
|
|
223
|
+
type CreateProductRequest struct {
|
|
224
|
+
Name string `json:"name" binding:"required,min=1,max=120"`
|
|
225
|
+
Price int64 `json:"price" binding:"required,gt=0"`
|
|
226
|
+
Stock int `json:"stock" binding:"gte=0"`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
type ProductResponse struct {
|
|
230
|
+
ID string `json:"id"`
|
|
231
|
+
Name string `json:"name"`
|
|
232
|
+
Price int64 `json:"price"`
|
|
233
|
+
Stock int `json:"stock"`
|
|
234
|
+
CreatedAt time.Time `json:"created_at"`
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```go
|
|
239
|
+
// internal/api/product/handler.go
|
|
240
|
+
package product
|
|
241
|
+
|
|
242
|
+
import (
|
|
243
|
+
"net/http"
|
|
244
|
+
"github.com/gin-gonic/gin"
|
|
245
|
+
"service/internal/application/product"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
type Handler struct {
|
|
249
|
+
createUC *product.CreateProductUseCase
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
func NewHandler(uc *product.CreateProductUseCase) *Handler {
|
|
253
|
+
return &Handler{createUC: uc}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func (h *Handler) Create(c *gin.Context) {
|
|
257
|
+
var req CreateProductRequest
|
|
258
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
259
|
+
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
p, err := h.createUC.Execute(c.Request.Context(), req.Name, req.Price, req.Stock)
|
|
263
|
+
if err != nil {
|
|
264
|
+
c.Error(err) // central error handler converts to ProblemDetails
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
c.JSON(http.StatusCreated, ProductResponse{
|
|
268
|
+
ID: p.ID.String(), Name: p.Name, Price: p.Price, Stock: p.Stock, CreatedAt: p.CreatedAt,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Rule**: handlers are thin — bind, validate (via `binding:` tags), call use case, serialize. Business logic stays in `application/`.
|
|
274
|
+
|
|
275
|
+
### Router + middleware
|
|
276
|
+
|
|
277
|
+
```go
|
|
278
|
+
// internal/api/product/router.go
|
|
279
|
+
package product
|
|
280
|
+
|
|
281
|
+
import "github.com/gin-gonic/gin"
|
|
282
|
+
|
|
283
|
+
func Register(r *gin.RouterGroup, h *Handler) {
|
|
284
|
+
products := r.Group("/products")
|
|
285
|
+
products.POST("", h.Create)
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```go
|
|
290
|
+
// cmd/server/main.go
|
|
291
|
+
package main
|
|
292
|
+
|
|
293
|
+
import (
|
|
294
|
+
"github.com/gin-gonic/gin"
|
|
295
|
+
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
|
296
|
+
// ... wire other deps
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
func main() {
|
|
300
|
+
cfg := loadConfig()
|
|
301
|
+
db := initDB(cfg)
|
|
302
|
+
runMigrations(db)
|
|
303
|
+
|
|
304
|
+
repo := productInfra.New(db)
|
|
305
|
+
createUC := productApp.NewCreateProductUseCase(repo)
|
|
306
|
+
handler := productAPI.NewHandler(createUC)
|
|
307
|
+
|
|
308
|
+
r := gin.New()
|
|
309
|
+
r.Use(gin.Recovery())
|
|
310
|
+
r.Use(otelgin.Middleware("products-service"))
|
|
311
|
+
r.Use(requestIDMiddleware())
|
|
312
|
+
r.Use(errorHandlerMiddleware()) // converts c.Error to RFC 9457
|
|
313
|
+
r.Use(rateLimiterMiddleware())
|
|
314
|
+
|
|
315
|
+
api := r.Group("/api/v1")
|
|
316
|
+
productAPI.Register(api, handler)
|
|
317
|
+
|
|
318
|
+
srv := &http.Server{
|
|
319
|
+
Addr: ":8080",
|
|
320
|
+
Handler: r,
|
|
321
|
+
ReadHeaderTimeout: 5 * time.Second,
|
|
322
|
+
WriteTimeout: 15 * time.Second,
|
|
323
|
+
IdleTimeout: 60 * time.Second,
|
|
324
|
+
}
|
|
325
|
+
log.Fatal(srv.ListenAndServe())
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Rule**: use `http.Server` with explicit timeouts. Default `srv.ListenAndServe()` has no `ReadHeaderTimeout` — slowloris vulnerability. Always set.
|
|
330
|
+
|
|
331
|
+
## 5. Testing
|
|
332
|
+
|
|
333
|
+
### Unit (table-driven, idiomatic Go)
|
|
334
|
+
|
|
335
|
+
```go
|
|
336
|
+
// internal/domain/product/entity_test.go
|
|
337
|
+
package product
|
|
338
|
+
|
|
339
|
+
import (
|
|
340
|
+
"testing"
|
|
341
|
+
"github.com/google/uuid"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
func TestReserve(t *testing.T) {
|
|
345
|
+
tests := []struct {
|
|
346
|
+
name string
|
|
347
|
+
stock int
|
|
348
|
+
qty int
|
|
349
|
+
wantErr bool
|
|
350
|
+
}{
|
|
351
|
+
{"decreases stock", 10, 3, false},
|
|
352
|
+
{"refuses zero", 10, 0, true},
|
|
353
|
+
{"refuses excess", 10, 11, true},
|
|
354
|
+
}
|
|
355
|
+
for _, tt := range tests {
|
|
356
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
357
|
+
p := Product{ID: uuid.New(), Stock: tt.stock}
|
|
358
|
+
_, err := p.Reserve(tt.qty)
|
|
359
|
+
if (err != nil) != tt.wantErr {
|
|
360
|
+
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Integration (Testcontainers + httptest)
|
|
368
|
+
|
|
369
|
+
```go
|
|
370
|
+
// tests/integration/api_test.go
|
|
371
|
+
package integration
|
|
372
|
+
|
|
373
|
+
import (
|
|
374
|
+
"context"
|
|
375
|
+
"net/http/httptest"
|
|
376
|
+
"testing"
|
|
377
|
+
"github.com/stretchr/testify/require"
|
|
378
|
+
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
func TestCreateProduct(t *testing.T) {
|
|
382
|
+
ctx := context.Background()
|
|
383
|
+
pg, err := postgres.Run(ctx, "postgres:16-alpine",
|
|
384
|
+
postgres.WithDatabase("test"),
|
|
385
|
+
postgres.WithUsername("test"),
|
|
386
|
+
postgres.WithPassword("test"),
|
|
387
|
+
)
|
|
388
|
+
require.NoError(t, err)
|
|
389
|
+
t.Cleanup(func() { pg.Terminate(ctx) })
|
|
390
|
+
|
|
391
|
+
// wire app with the postgres connection
|
|
392
|
+
r := buildRouter(t, pg)
|
|
393
|
+
ts := httptest.NewServer(r)
|
|
394
|
+
t.Cleanup(ts.Close)
|
|
395
|
+
|
|
396
|
+
// POST + assert response
|
|
397
|
+
// ...
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Rule**: never SQLite for integration tests if prod is Postgres. Testcontainers Go is the standard.
|
|
402
|
+
|
|
403
|
+
### Mutation
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
go-mutesting --exec internal/domain/... internal/application/...
|
|
407
|
+
# Target: killed/total >= 70%
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## 6. Build & run commands
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
# Setup
|
|
414
|
+
go mod download
|
|
415
|
+
go mod tidy
|
|
416
|
+
|
|
417
|
+
# Run
|
|
418
|
+
go run ./cmd/server
|
|
419
|
+
|
|
420
|
+
# Build (with version info)
|
|
421
|
+
go build -ldflags "-X main.version=$(git rev-parse HEAD)" -o bin/server ./cmd/server
|
|
422
|
+
|
|
423
|
+
# Cross-compile (Linux from any OS)
|
|
424
|
+
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bin/server-linux ./cmd/server
|
|
425
|
+
|
|
426
|
+
# Tests
|
|
427
|
+
go test ./... # all
|
|
428
|
+
go test -race ./... # data race detector (slow but mandatory in CI)
|
|
429
|
+
go test -coverprofile=cov.out -covermode=atomic ./...
|
|
430
|
+
go tool cover -func=cov.out | tail -1 # total coverage
|
|
431
|
+
|
|
432
|
+
# Lint
|
|
433
|
+
golangci-lint run ./...
|
|
434
|
+
|
|
435
|
+
# Security
|
|
436
|
+
govulncheck ./...
|
|
437
|
+
gosec ./...
|
|
438
|
+
|
|
439
|
+
# Migrations (golang-migrate)
|
|
440
|
+
migrate -path migrations -database "$DATABASE_URL" up
|
|
441
|
+
migrate -path migrations -database "$DATABASE_URL" down 1
|
|
442
|
+
migrate create -ext sql -dir migrations -seq create_products
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## 7. Security (per ADR-007 + ADR-027 — MANDATORY section)
|
|
446
|
+
|
|
447
|
+
### 7.1 Authentication & Authorization
|
|
448
|
+
|
|
449
|
+
- **JWT**: `github.com/golang-jwt/jwt/v5`. RS256 preferred; HS256 acceptable single-service. Set `Issuer`, `Audience`, `ExpiresAt` claims. Validate `kid` for rotation.
|
|
450
|
+
- **OAuth2**: `golang.org/x/oauth2` with provider-specific endpoints. PKCE for SPAs.
|
|
451
|
+
- **Password hashing**: `golang.org/x/crypto/bcrypt` with cost 12 minimum. NEVER plaintext, NEVER MD5/SHA1, NEVER unsalted SHA256.
|
|
452
|
+
- **API keys (internal)**: `crypto/rand` + `base64url`. Hash before storage (`sha256` is acceptable for non-user secrets).
|
|
453
|
+
- **Authorization**: middleware that parses JWT, populates `gin.Context` with user/roles, then per-route middleware checks role. Object-level checks inside the use case.
|
|
454
|
+
|
|
455
|
+
### 7.2 CORS
|
|
456
|
+
|
|
457
|
+
```go
|
|
458
|
+
import "github.com/gin-contrib/cors"
|
|
459
|
+
|
|
460
|
+
r.Use(cors.New(cors.Config{
|
|
461
|
+
AllowOrigins: []string{"https://app.example.com"}, // NEVER ["*"] in prod
|
|
462
|
+
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
463
|
+
AllowHeaders: []string{"Authorization", "Content-Type"},
|
|
464
|
+
AllowCredentials: true,
|
|
465
|
+
MaxAge: 12 * time.Hour,
|
|
466
|
+
}))
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 7.3 Validation & input sanitization
|
|
470
|
+
|
|
471
|
+
- **SQL injection**: GORM parameterizes by default. NEVER `db.Raw(fmt.Sprintf("... %s ...", input))` — use `db.Raw("... ?", input)`.
|
|
472
|
+
- **`validator` tags**: `binding:"required,min=1,max=120"` on every request struct field. Custom validators registered at startup.
|
|
473
|
+
- **Path traversal**: `filepath.Clean` + check the resolved path is inside an allowlisted base before opening.
|
|
474
|
+
- **NoSQL injection (Mongo)**: use the typed bson builder, not `bson.M{}` dict concatenation with user input.
|
|
475
|
+
|
|
476
|
+
### 7.4 Secrets management
|
|
477
|
+
|
|
478
|
+
- `envconfig` struct loaded at startup, validation via `validator` tags.
|
|
479
|
+
- `.env` gitignored; `env.example` committed.
|
|
480
|
+
- Prefer Secrets Manager (AWS / GCP / Vault) in prod over `.env`. `envconfig` doesn't read from there directly — wrap with a `secretsLoader` interface.
|
|
481
|
+
|
|
482
|
+
### 7.5 Rate limiting
|
|
483
|
+
|
|
484
|
+
```go
|
|
485
|
+
import "github.com/gin-contrib/limiter"
|
|
486
|
+
|
|
487
|
+
// 100 req/min/IP
|
|
488
|
+
r.Use(limiter.NewRateLimiter(limiter.Rate{
|
|
489
|
+
Period: time.Minute,
|
|
490
|
+
Limit: 100,
|
|
491
|
+
}))
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
- `5/min` on `/login`, `/signup`, `/password-reset`. Use per-route group:
|
|
495
|
+
|
|
496
|
+
```go
|
|
497
|
+
auth := r.Group("/auth", limiter.NewRateLimiter(...))
|
|
498
|
+
auth.POST("/login", h.Login)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
- Behind reverse proxy: trust `X-Forwarded-For` only from known proxy IPs.
|
|
502
|
+
|
|
503
|
+
### 7.6 OWASP Top 10 mapping
|
|
504
|
+
|
|
505
|
+
| OWASP | Mitigation in Go + Gin |
|
|
506
|
+
|---|---|
|
|
507
|
+
| A01 Broken Access Control | JWT middleware + per-route role check; object-level check in use case; default-deny |
|
|
508
|
+
| A02 Cryptographic Failures | bcrypt cost 12; TLS at reverse proxy; HSTS via `c.Header("Strict-Transport-Security", ...)` in middleware |
|
|
509
|
+
| A03 Injection | GORM parameterization; `validator` tags on every struct; never `db.Raw` with f-strings |
|
|
510
|
+
| A04 Insecure Design | DDD layering (`domain/` no Gin/GORM); use case = single public method per type; ADRs document deviations |
|
|
511
|
+
| A05 Security Misconfiguration | `gin.SetMode(gin.ReleaseMode)` in prod; `Server.ReadHeaderTimeout` explicit; `cors` allowlist explicit |
|
|
512
|
+
| A06 Vulnerable Components | `govulncheck ./...` in CI; `dependabot` or `renovate` for `go.mod` bumps |
|
|
513
|
+
| A07 Auth Failures | rate-limit on auth endpoints; bcrypt with cost 12; account lock via Redis counter |
|
|
514
|
+
| A08 Data Integrity | `go mod verify`; `go.sum` committed; SBOM via `syft`/`cyclonedx-go` |
|
|
515
|
+
| A09 Logging Failures | Structured JSON via `slog` (stdlib 1.21+) or `zerolog`; correlation ID middleware; **NEVER** log request body raw (PII) |
|
|
516
|
+
| A10 SSRF | Centralized `http.Client` with allowlist of outbound hosts; reject private CIDRs (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.169.254`) by default |
|
|
517
|
+
|
|
518
|
+
### 7.7 LGPD / GDPR / compliance specifics
|
|
519
|
+
|
|
520
|
+
- **PII tagging**: convention `// pii: true` in struct comments + a custom `golangci-lint` rule (`revive` config) to flag missing tags on string fields named `Email`, `CPF`, `Document`.
|
|
521
|
+
- **Soft delete**: `DeletedAt *time.Time` column + GORM `gorm.DeletedAt` type. NEVER hard delete user-owned data.
|
|
522
|
+
- **Data subject access**: `GET /api/v1/me/export` returns user data as JSON or zipped CSV.
|
|
523
|
+
- **Erasure**: `DELETE /api/v1/me` sets PII fields to anonymized values while preserving FKs.
|
|
524
|
+
- **Encryption at rest**: PostgreSQL TDE (cloud-managed) or column-level via `pgcrypto` + Go's `crypto/aes` for AES-256-GCM application-level.
|
|
525
|
+
|
|
526
|
+
## 8. Anti-patterns (block in code-review)
|
|
527
|
+
|
|
528
|
+
| ❌ Bad | ✅ Good | Why |
|
|
529
|
+
|---|---|---|
|
|
530
|
+
| `http.Get(url)` directly | `client := &http.Client{Timeout: 10*time.Second}; client.Get(url)` | No timeout = leaked goroutines on slow upstream |
|
|
531
|
+
| `srv.ListenAndServe()` without timeouts | `&http.Server{ReadHeaderTimeout: 5*time.Second, ...}` | Slowloris attack works against default |
|
|
532
|
+
| `db.Raw(fmt.Sprintf("SELECT ... WHERE id = %s", id))` | `db.Raw("SELECT ... WHERE id = ?", id)` | SQL injection via concatenation |
|
|
533
|
+
| `gin.SetMode(gin.DebugMode)` in prod | `gin.SetMode(gin.ReleaseMode)` from env | Debug mode exposes routes + verbose errors |
|
|
534
|
+
| `float64` or `float32` for money | `int64` (cents) or `shopspring/decimal` | Float precision loss is real |
|
|
535
|
+
| `panic` inside handler not caught | `gin.Recovery()` middleware first in chain | Single bad request crashes the worker |
|
|
536
|
+
| Goroutine spawned in handler without `defer wg.Done()` | Explicit `errgroup.Group` with `ctx` propagation | Goroutine leaks on early return |
|
|
537
|
+
| GORM `AutoMigrate` in prod startup | `golang-migrate` with reviewed SQL files | Schema changes need explicit review |
|
|
538
|
+
| Catching `error` and ignoring with `_` | Always handle: `if err != nil { return err }` OR explicit comment why ignoring | Silent errors = silent bugs |
|
|
539
|
+
| Returning ORM struct directly from handler | DTO with `json:` tags; explicit conversion | API contract leaks data model |
|
|
540
|
+
| Hardcoded `time.Sleep` in tests | Channels + `time.After` for timeouts | Flaky tests on slow CI |
|
|
541
|
+
| `interface{}` (or `any`) when a concrete type fits | Generic `func F[T any](v T)` or concrete type | Type safety wins |
|
|
542
|
+
|
|
543
|
+
## 9. Migration hints — Gin 1.9 → 1.10+
|
|
544
|
+
|
|
545
|
+
Breaking changes worth flagging when `migrator` agent runs Gin 1.9 → 1.10+:
|
|
546
|
+
|
|
547
|
+
- **Go 1.21 minimum** (Gin 1.10); 1.23 recommended for `slog` + generics improvements.
|
|
548
|
+
- **`Context.IsAborted()`** signature unchanged but rendered errors flow through `c.Error(...)` more strictly. Audit any custom error responses.
|
|
549
|
+
- **`gin.H`** is unchanged but consider replacing with typed `gin.H{}` returns by named response structs for OpenAPI compatibility.
|
|
550
|
+
- **`Routes()` method** on `Engine`: minor signature consolidation. Custom route enumerators may need adjustment.
|
|
551
|
+
- **HTTP/2** is enabled by default. If your service is behind a proxy doing HTTP/1.1 only, explicitly disable: `srv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))`.
|
|
552
|
+
- **`testify`** mock vs `gomock`: if migrating away from `testify/mock`, `gomock` 0.5 has go-generate based generation that integrates better with new code.
|
|
553
|
+
|
|
554
|
+
Hand off to `migrator` with: current Gin version, current Go toolchain, list of custom middleware that touches `gin.Context` internals.
|
|
555
|
+
|
|
556
|
+
## 10. References
|
|
557
|
+
|
|
558
|
+
- [Gin official docs](https://gin-gonic.com/docs/)
|
|
559
|
+
- [GORM 1.25 docs](https://gorm.io/docs/)
|
|
560
|
+
- [golang-migrate docs](https://github.com/golang-migrate/migrate/blob/master/GETTING_STARTED.md)
|
|
561
|
+
- [go-playground/validator](https://github.com/go-playground/validator)
|
|
562
|
+
- [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck)
|
|
563
|
+
- [gosec](https://github.com/securego/gosec)
|
|
564
|
+
- [Testcontainers Go](https://golang.testcontainers.org/)
|
|
565
|
+
- [OpenTelemetry Go](https://opentelemetry.io/docs/languages/go/)
|
|
566
|
+
- [OWASP Go Secure Coding Practices](https://github.com/OWASP/Go-SCP)
|
|
567
|
+
- ADR-007 (Senior+ gate thresholds — coverage ≥85%, mutation ≥70%)
|
|
568
|
+
- ADR-026 (Generic agents + stack packs architecture)
|
|
569
|
+
- ADR-027 (Pack governance — frontmatter + security mandatory + CODEOWNERS + annual review)
|
|
570
|
+
- ADR-029 (Canonical pack format — this document follows it)
|