packages/shared-skills/skills/programming/references/go/backend-stack.md
The canonical production HTTP service skeleton. Distilled from the CLIProxyAPI codebase — a real proxy serving OpenAI / Gemini / Claude / Codex APIs in production, with SSE streaming, WebSocket upgrades, request logging, and hot-reload config.
If you are tempted to pick echo or chi instead, see libraries.md — gin wins on ecosystem, not technical merit, and the win is large enough to matter.
go.modmodule github.com/your-org/myservice
go 1.23
require (
github.com/gin-gonic/gin v1.10.1
github.com/go-playground/validator/v10 v10.22.1
github.com/caarlos0/env/v11 v11.2.2
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.6
golang.org/x/sync v0.18.0
)
cmd/server/main.go # ≤ 50 LOC; flags → run.Execute(ctx)
internal/
cmd/run.go # ~150 LOC; signal handling, config load, server.Run
config/config.go # env-driven Config struct
api/
server.go # gin.Engine setup, route mounting, http.Server
middleware/
request_id.go
request_logging.go
auth.go
recovery.go
cors.go
handlers/
users.go # one file per resource
streams.go # SSE / WebSocket endpoints
domain/ # smart-constructor types (Email, UserID, ...)
service/ # business logic
store/ # pgx + sqlc
obs/
logger.go # slog setup
cmd/server/main.gopackage main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/your-org/myservice/internal/cmd"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := cmd.Execute(ctx); err != nil {
slog.Error("fatal", slog.Any("err", err))
os.Exit(1)
}
}
That is the entire main. Anything more is a smell.
internal/config/config.gopackage config
import (
"time"
"github.com/caarlos0/env/v11"
)
type Config struct {
Host string `env:"HOST" envDefault:"0.0.0.0"`
Port int `env:"PORT" envDefault:"8080"`
DatabaseURL string `env:"DATABASE_URL,required"`
ReadTimeout time.Duration `env:"READ_TIMEOUT" envDefault:"15s"`
WriteTimeout time.Duration `env:"WRITE_TIMEOUT" envDefault:"30s"`
ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" envDefault:"20s"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
LogFormat string `env:"LOG_FORMAT" envDefault:"json"`
Env string `env:"ENV" envDefault:"development"`
}
func Load() (Config, error) {
var cfg Config
if err := env.Parse(&cfg); err != nil {
return Config{}, err
}
return cfg, nil
}
internal/obs/logger.gopackage obs
import (
"context"
"log/slog"
"os"
)
type ctxKey struct{ name string }
var requestIDKey = ctxKey{"request_id"}
func NewLogger(level, format string) *slog.Logger {
var lvl slog.Level
_ = lvl.UnmarshalText([]byte(level))
opts := &slog.HandlerOptions{Level: lvl, AddSource: true}
var h slog.Handler
switch format {
case "text":
h = slog.NewTextHandler(os.Stdout, opts)
default:
h = slog.NewJSONHandler(os.Stdout, opts)
}
return slog.New(&ctxHandler{Handler: h})
}
// ctxHandler pulls request_id from ctx into every log line.
type ctxHandler struct{ slog.Handler }
func (h *ctxHandler) Handle(ctx context.Context, r slog.Record) error {
if id, ok := ctx.Value(requestIDKey).(string); ok && id != "" {
r.AddAttrs(slog.String("request_id", id))
}
return h.Handler.Handle(ctx, r)
}
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
internal/api/server.gopackage api
import (
"context"
"fmt"
"log/slog"
"net/http"
"github.com/gin-gonic/gin"
"github.com/your-org/myservice/internal/api/handlers"
"github.com/your-org/myservice/internal/api/middleware"
"github.com/your-org/myservice/internal/config"
)
type Server struct {
cfg config.Config
srv *http.Server
logger *slog.Logger
}
func New(cfg config.Config, logger *slog.Logger, h *handlers.Handler) *Server {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// Middleware order matters — see "Middleware ordering" below.
r.Use(
middleware.RequestID(), // 1. assign request_id first
middleware.Recovery(logger), // 2. recovery wraps everything
middleware.RequestLogger(logger),
middleware.CORS(),
)
h.Mount(r)
return &Server{
cfg: cfg,
logger: logger,
srv: &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Handler: r,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
},
}
}
func (s *Server) Run(ctx context.Context) error {
errCh := make(chan error, 1)
go func() {
s.logger.InfoContext(ctx, "server starting",
slog.String("addr", s.srv.Addr))
if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
close(errCh)
}()
select {
case <-ctx.Done():
s.logger.InfoContext(ctx, "shutdown signal received")
shutdownCtx, cancel := context.WithTimeout(
context.Background(), s.cfg.ShutdownTimeout)
defer cancel()
return s.srv.Shutdown(shutdownCtx)
case err := <-errCh:
return err
}
}
Notes:
gin.New() not gin.Default() — Default() adds Logger() (text format, not slog) and Recovery() (no logger injection). We replace both.gin.SetMode(gin.ReleaseMode) silences debug output. Production assumed.http.Server with explicit timeouts. The default nil timeouts are a DoS waiting to happen.Shutdown(shutdownCtx) gives in-flight requests up to ShutdownTimeout to finish.RequestID → Recovery → Logger → CORS → Auth → Handler
(1) (2) (3) (4) (5)
// Public routes — no auth
api := r.Group("/api/v1")
{
api.POST("/auth/login", h.Login)
api.GET("/healthz", h.Healthz)
}
// Authenticated routes
authed := r.Group("/api/v1", middleware.Auth(authSvc))
{
authed.GET("/users/:id", h.GetUser)
authed.POST("/users", h.CreateUser)
}
// Admin-only routes
admin := r.Group("/api/v1/admin",
middleware.Auth(authSvc),
middleware.RequireRole("admin"))
{
admin.GET("/users", h.ListAllUsers)
}
middleware/request_id.gopackage middleware
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/your-org/myservice/internal/obs"
)
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader("X-Request-ID")
if id == "" {
id = uuid.Must(uuid.NewV7()).String()
}
c.Request = c.Request.WithContext(obs.WithRequestID(c.Request.Context(), id))
c.Header("X-Request-ID", id)
c.Next()
}
}
middleware/recovery.gopackage middleware
import (
"log/slog"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
func Recovery(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
logger.ErrorContext(c.Request.Context(), "panic recovered",
slog.Any("panic", r),
slog.String("stack", string(debug.Stack())),
)
if !c.Writer.Written() {
c.JSON(http.StatusInternalServerError,
gin.H{"error": "internal_error"})
}
c.Abort()
}
}()
c.Next()
}
}
middleware/request_logging.gofunc RequestLogger(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
logger.InfoContext(c.Request.Context(), "http request",
slog.String("method", c.Request.Method),
slog.String("path", c.Request.URL.Path),
slog.Int("status", c.Writer.Status()),
slog.Int("bytes", c.Writer.Size()),
slog.Duration("elapsed", time.Since(start)),
slog.String("ip", c.ClientIP()),
)
}
}
The sloglint linter enforces typed attrs (slog.String(...)) over slog.Any("path", ...). Keep the form.
middleware/cors.gofunc CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "*")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
Note the explicit OPTIONS short-circuit — preflight must NOT go through Auth.
package handlers
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/your-org/myservice/internal/domain"
"github.com/your-org/myservice/internal/httperr"
"github.com/your-org/myservice/internal/service"
)
type Handler struct {
Users *service.UserService
}
func (h *Handler) Mount(r gin.IRouter) {
api := r.Group("/api/v1")
api.POST("/users", h.CreateUser)
api.GET("/users/:id", h.GetUser)
}
type createUserReq struct {
Email string `json:"email" binding:"required,email"`
Username string `json:"username" binding:"required,alphanum,min=3,max=32"`
}
func (h *Handler) CreateUser(c *gin.Context) {
var req createUserReq
if err := c.ShouldBindJSON(&req); err != nil {
writeBindingError(c, err)
return
}
email, err := domain.NewEmail(req.Email)
if err != nil {
httperr.Write(c, err)
return
}
username, err := domain.NewUsername(req.Username)
if err != nil {
httperr.Write(c, err)
return
}
user, err := h.Users.Create(c.Request.Context(), email, username)
if err != nil {
httperr.Write(c, err)
return
}
c.JSON(http.StatusCreated, user)
}
func writeBindingError(c *gin.Context, err error) {
var vErr validator.ValidationErrors
if errors.As(err, &vErr) {
out := make(map[string]string, len(vErr))
for _, fe := range vErr {
out[fe.Field()] = fe.Tag()
}
c.JSON(http.StatusBadRequest, gin.H{"errors": out})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
}
See data-modeling.md for the validator tag reference; see error-handling.md for the httperr.Write funnel.
CLIProxyAPI streams OpenAI-compatible SSE for hundreds of concurrent clients. The pattern:
func (h *Handler) StreamChat(c *gin.Context) {
ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel()
// 1. Set SSE headers BEFORE writing any body
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no") // disable nginx buffering
// 2. Obtain the flusher — REQUIRED for streaming
flusher, ok := c.Writer.(http.Flusher)
if !ok {
httperr.Write(c, errors.New("streaming unsupported"))
return
}
// 3. Pull chunks from upstream
chunks, errs := h.svc.StreamCompletions(ctx, req)
for {
select {
case <-ctx.Done():
return // client disconnected, ctx cancelled
case chunk, ok := <-chunks:
if !ok {
fmt.Fprint(c.Writer, "data: [DONE]\n\n")
flusher.Flush()
return
}
fmt.Fprintf(c.Writer, "data: %s\n\n", chunk)
flusher.Flush()
case err := <-errs:
// Error mid-stream — emit as SSE event and bail
fmt.Fprintf(c.Writer, "event: error\ndata: %s\n\n", err.Error())
flusher.Flush()
return
}
}
}
Key facts:
Write. Otherwise gin auto-sets Content-Type: text/plain.c.Writer.(http.Flusher) is the streaming primitive. Without flusher.Flush(), the response is buffered and arrives as one blob at the end.<-ctx.Done(). A disconnected client must stop upstream work — otherwise you generate tokens for nothing.\n\n per event is wire-mandatory for SSE parsing. Missing it = the client never sees the event.import "github.com/gorilla/websocket" // still the canonical WS lib in 2026
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
// tighten in production
return true
},
}
func (h *Handler) WebSocketEcho(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
slog.ErrorContext(c.Request.Context(), "ws upgrade failed", slog.Any("err", err))
return
}
defer conn.Close()
for {
mt, msg, err := conn.ReadMessage()
if err != nil { return }
if err := conn.WriteMessage(mt, msg); err != nil { return }
}
}
For long-lived connections, use conn.SetReadDeadline + SetPongHandler for keepalive. CLIProxyAPI's wsrelay package is a reference implementation.
package store
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("parse dsn: %w", err)
}
cfg.MaxConns = 25
cfg.MinConns = 5
cfg.MaxConnLifetime = time.Hour
cfg.MaxConnIdleTime = 30 * time.Minute
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping: %w", err)
}
return pool, nil
}
See sqlc-pgx.md for queries.
func (h *Handler) Healthz(c *gin.Context) {
if err := h.pool.Ping(c.Request.Context()); err != nil {
c.JSON(503, gin.H{"db": "down", "error": err.Error()})
return
}
c.JSON(200, gin.H{"ok": true})
}
Mount BEFORE auth. Health checks must be unauthenticated.
func TestCreateUser_returns_201_for_valid_input(t *testing.T) {
// Given
h := newTestHandler(t)
r := gin.New()
h.Mount(r)
body := `{"email":"[email protected]","username":"alice"}`
req := httptest.NewRequest("POST", "/api/v1/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
// When
r.ServeHTTP(rec, req)
// Then
require.Equal(t, http.StatusCreated, rec.Code)
var got struct{ ID string `json:"id"` }
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
require.NotEmpty(t, got.ID)
}
See testing.md for full patterns (testcontainers integration, table-driven, goleak).
http.Server graceful shutdown: https://pkg.go.dev/net/http#Server.Shutdown