Clear and stable contracts beat ad hoc handlers. Every mutation goes through binding, validation, and authorization on the server.
Stack and scope
| Ask | Role |
|---|---|
| Gin | Router, middleware, binding, context |
| encoding/json | JSON request/response (or the project's lib) |
| context | Cancellation and request values |
| validator | Tags binding:"required,..." of Gin |
For background concurrency, WebSocket, SQL, testing, ops, or i18n: golang-goroutines, golang-websocket, golang-sqlc, golang-unit-testing, golang-production, i18n-backend. Language: golang. HTTP contracts: api-design.
Recommended organization
cmd/api/main.go
internal/
config/
http/
router.go
middleware/
handlers/
domain/ # ou services/ por bounded context
repository/
mainjust wiring: config, deps,r.Run.- Thin handlers; business logic in services.
- Preserve the layout and libs already adopted by the project.
Non-negotiable rules
- Handlers:
func(c *gin.Context); never ignorec.Request.Context(). - Bind with
ShouldBindJSON/ShouldBindQuery/ URI; fail with400typed. - Authn ≠ Authz: authenticated does not authorize ownership/role.
- Do not expose internal structs with secrets; use response DTOs.
- Stable errors:
{ "code", "message", "details?" }+ correct HTTP status codes. - Config and secrets via env; no hardcoding.
- Graceful shutdown with
http.Server+ signal (not justr.Runblind in production).
Router and groups
r := gin.New()
r.Use(gin.Recovery(), RequestID(), Logger())
v1 := r.Group("/api/v1")
{
v1.GET("/health", health)
auth := v1.Group("/")
auth.Use(Authenticate())
auth.GET("/todos", listTodos)
auth.POST("/todos", createTodo)
admin := v1.Group("/admin")
admin.Use(Authenticate(), RequireRole("admin"))
admin.GET("/users", listUsers)
}
- Versioning (
/api/v1) when the API is public or multi-client. - Auth middleware on the group, not copied into each route.
- Typed path params; reject invalid IDs early.
Binding, validation, and errors
type CreateTodoRequest struct {
Title string `json:"title" binding:"required,min=1,max=200"`
}
func createTodo(c *gin.Context) {
var req CreateTodoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"code": "validation_error", "message": "invalid request", "details": err.Error(),
})
return
}
// service call with c.Request.Context()
}
- Status:
400,401,403,404,409,422,429,500as appropriate. codemachine-readable;messagesafe; no stack in production.- Distinguish validation (client) from internal failure (server).
Pagination and filters
- Cursor-based for large lists; offset only when simple and limited.
- Limit page size; reject abusive values.
- Deterministic ordering.
- Document supported filters.
Authorization
- Extract identity in middleware; store it in
c.Setwith a typed/constant key. - Verify ownership/roles in the handler or service on every sensitive route.
- Rate limit on auth and expensive endpoints when the risk exists.
- Explicit CORS if there is a separate frontend.
Observability
- Log request id, user id (if any), route, status, latency.
- Do not log secrets or full sensitive payloads.
- Propagate
context.Contextto the DB and HTTP clients.
Anti-patterns
- Entire business logic in the handler
c.Getwithout checking existence / type200with an error in the body without a convention- Ignore request cancellation
- Validation only on the frontend
- Gin in debug mode in production
- Panic on the happy path instead of an HTTP error
Flow when implementing a feature
- Inspect existing router, middleware, and handlers.
- Define request/response DTO + validation rules.
- Service with the business rule (context-aware).
- Gin handler with authz and correct status codes.
- Tests for risk cases (
golang-unit-testing). - Update config/env/docs only for what changed.
Completion criteria
- Routes grouped and versioned when applicable
- Input validated; consistent errors and status codes
- Authz covered on sensitive routes
- Safe pagination/limits
- Context propagated; no leakage of internal fields
- Contract usable by at least one real client in the repo