Gin Architecture
Targets Gin 1.12 on Go 1.26. Companion to go-architect, rest-api-architect, and sql-architect. Implementation skeletons in RECIPES.md; pinned deps in STACK.md.
1. Project structure — feature-based
One folder per bounded context. Each feature owns its routes, service, repo, DTOs, and SQL files. Mirrors fastapi-architect so polyglot teams can navigate either side. Full tree in RECIPES.md.
handlers.godepends onservice.go; never reaches intorepo.godirectly.service.gois pure Go — noginimports. Easy to unit-test without a fake*gin.Context.dto.goholds wire types withjson:andbinding:(validator) tags. Never reuse domain structs as DTOs — that's how internal fields leak into the API.
2. Routing & versioning
URL-prefix versioning via route groups. One group per version, one sub-group per feature. Registration skeleton in RECIPES.md.
- One
Register(rg, deps)function per feature — keepsmain.gothin. - Path params typed at parse time:
id, err := uuid.Parse(c.Param("user_id"))— return 400 on parse failure. - Use
gin.RouterGroup, not bareEngine.GET, so versioning + per-group middleware stays clean.
3. Request validation
Use struct tags with go-playground/validator (canonical per go-architect). Bind via c.ShouldBindJSON / c.ShouldBindUri / c.ShouldBindQuery — never c.MustBindWith (panics; we don't panic in handlers).
type CreateUserReq struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=12"`
}
binding:"required"on every field that isn't truly optional.- Custom validators registered once at startup:
validate.RegisterValidation("uuid_v7", isUUIDv7). binding:"omitempty"on PATCH partial-update DTOs — let absent fields mean "leave unchanged."
4. Response shaping
- Return DTOs, not domain types.
c.JSON(200, dto.UserResponse{...})— DTOs control what leaks. json:"-"on any DTO field that should never serialize (passwords, internal IDs).- Status code with
c.JSON(http.StatusCreated, ...)— explicit, not Gin's default 200. - Empty body uses
c.Status(http.StatusNoContent), notc.JSON(204, nil)(sendsnull).
5. Dependency injection
Explicit constructors per go-architect §3. No globals, no init() magic. Dependencies live in a Deps struct wired in main.go. For larger graphs use uber-go/fx (canonical per go-architect); for services with <10 dependencies, hand-wired is clearer.
6. Lifespan & graceful shutdown
Open shared resources in main.go, never per-request. Close them on shutdown signal via signal.NotifyContext (Go 1.26 records which signal fired). Full skeleton in RECIPES.md.
ReadHeaderTimeoutis mandatory — without it, Slowloris can pin connections forever.- Graceful shutdown timeout > longest expected request duration.
7. Authentication & authorization
Patterns (in-house JWT vs external IdP, Argon2id, JWT lifetimes, JWKS verification, switching criterion) live in rest-api-architect/AUTH_PATTERNS.md. Gin specifics:
- Pattern A — in-house JWT via
golang-jwt/jwt/v5+argon2fromgolang.org/x/crypto. Middleware skeleton in RECIPES.md. - Pattern B — external IdP:
jwt.ParseWithClaimswith aKeyfuncthat resolves keys via a JWKS client. Cache JWKS in-process with TTL. - Authorization per route, never global —
RequireScope(...)composed alongsideAuthRequired(secret)on the route declaration (see RECIPES.md).
8. Error handling — RFC 7807 middleware
A central problem package emits application/problem+json (per rest-api-architect §7). Handlers either call problem.Render(c, p) directly or c.Error(err) and let the recovery middleware convert. Renderer in RECIPES.md.
- One handler per domain-error family — map known sentinel errors to
Problemtypes in a single switch. gin.Recoverywith a customRecoveryHandlerthat emits a 500Problemwithcorrelation_id— never a stack trace.
9. Middleware order
Outermost first; order matters.
r := gin.New() // not gin.Default — we set logging ourselves
r.Use(
middleware.RequestID(), // 1. assign correlation_id
middleware.SLog(), // 2. structured access log
gin.Recovery(), // 3. convert panics → 500 problem
middleware.CORS(cfg.CORS), // 4. preflight handling
gzip.Gzip(gzip.DefaultCompression), // 5. response compression
)
- Never
gin.Default()in production — its logger writes unstructured text to stdout. Useslog(per go-architect). - Auth is per-route middleware, never global (see §7).
- Recovery before CORS — a panic that bypasses CORS handler returns no headers; the browser shows a misleading CORS error.
10. Concurrency, not "background tasks"
Go has no FastAPI-style BackgroundTasks. To run work after the response, derive a new context.Context from context.Background() (request context is cancelled the moment the response writes), set a timeout, log errors. Anything serious (retryable, distributed, scheduled) belongs in a real task queue, not a goroutine.
11. Testing
httptest.NewRecorder + the engine directly — no real socket. Skeleton in RECIPES.md.
- Test-DB strategy: mirror
sql-architect— wrap each test in a rolled-back transaction, or use a per-test schema withgolang-migrate. - Table-driven tests (per go-architect §9) for request validation paths — one row per (input, expected status, expected error type).
12. OpenAPI generation
- Default:
swaggo/swag—// @annotation comments above handlers;swag initgeneratesdocs/swagger.jsonandswagger.yaml. Predictable, mature. - For full OpenAPI 3.1 control:
getkin/kin-openapi— write the spec in code; serve it; use it to validate requests at runtime. More work but no annotation noise. - CI snapshot-tests the spec (per rest-api-architect §15).
- Internal endpoints excluded with
// @Hidden(swag) or by not registering them in the spec route group.