Padrões de serviço
O que todo serviço desta plataforma implementa igual.
Nesta página
- Layout de Diretorio (cada servico Go)
- Dependencias Padrao (go.mod)
- config.go (padrao)
- main.go (padrao)
- HTTP Server (padrao)
- Error Response Format (SES-like)
- Logging
- ID Generation
- Graceful Shutdown
- Testing
- Dockerfile Multi-stage
- Makefile
- Endpoints comuns a todos os servicos
- Service-to-Service Auth
- Frontend Patterns (Next.js)
Service Patterns - Asender (Go)
Padroes comuns pra todos os servicos Go. Cada servico segue este layout.
Layout de Diretorio (cada servico Go)
asender-<service>/
├── cmd/
│ └── server/
│ └── main.go # Entry point
├── internal/
│ ├── config/
│ │ └── config.go # env vars via envconfig
│ ├── db/
│ │ ├── db.go # pgx pool factory
│ │ ├── migrations/ # SQL files numbered
│ │ │ └── 001_init.up.sql
│ │ │ └── 001_init.down.sql
│ │ └── queries/ # sqlc .sql files (ou query methods)
│ ├── http/
│ │ ├── server.go # chi router + middleware
│ │ ├── handlers/ # per-resource handlers
│ │ └── middleware/ # local middleware (auth, tenant, etc)
│ ├── service/ # business logic, framework-agnostic
│ │ └── ...
│ └── repo/ # DB access layer (wraps sqlc queries)
│ └── ...
├── migrations/ # (alt location - some prefer root)
├── Dockerfile
├── Makefile
├── go.mod
├── go.sum
├── .env.example
└── README.md
Dependencias Padrao (go.mod)
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/jackc/pgx/v5 v5.7.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/nats-io/nats.go v1.37.0
github.com/redis/go-redis/v9 v9.6.1
github.com/rs/xid v1.6.0
golang.org/x/crypto v0.27.0
)
(Cada servico inclui so o que usa.)
config.go (padrao)
package config
import "github.com/kelseyhightower/envconfig"
type Config struct {
Env string `envconfig:"ENV" default:"development"`
HTTPPort int `envconfig:"HTTP_PORT" default:"8080"`
LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
RedisURL string `envconfig:"REDIS_URL" default:"redis://localhost:6379"`
NatsURL string `envconfig:"NATS_URL" default:"nats://localhost:4222"`
ServiceSecret string `envconfig:"SERVICE_SECRET" required:"true"`
JWTSecret string `envconfig:"JWT_SECRET" required:"true"`
AuthServiceURL string `envconfig:"AUTH_SERVICE_URL" default:"http://asender-auth:8080"`
CoreServiceURL string `envconfig:"CORE_SERVICE_URL" default:"http://asender-core:8080"`
}
func Load() (*Config, error) {
var c Config
err := envconfig.Process("", &c)
return &c, err
}
main.go (padrao)
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"asender-<service>/internal/config"
"asender-<service>/internal/http/server"
)
func main() {
cfg, err := config.Load()
if err != nil {
slog.Error("config load failed", "err", err)
os.Exit(1)
}
logger := newLogger(cfg.LogLevel)
slog.SetDefault(logger)
srv, err := server.New(cfg, logger)
if err != nil {
slog.Error("server init failed", "err", err)
os.Exit(1)
}
httpSrv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.HTTPPort),
Handler: srv.Router(),
ReadHeaderTimeout: 10 * time.Second,
}
// Graceful shutdown
go func() {
slog.Info("server listening", "port", cfg.HTTPPort)
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("http listen", "err", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := httpSrv.Shutdown(ctx); err != nil {
slog.Error("shutdown", "err", err)
}
}
HTTP Server (padrao)
package server
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Server struct {
cfg *config.Config
router *chi.Mux
// ...deps
}
func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(loggingMiddleware(logger))
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
s := &Server{cfg: cfg, router: r}
s.routes()
return s, nil
}
func (s *Server) Router() http.Handler {
return s.router
}
func (s *Server) routes() {
s.router.Get("/healthz", s.handleHealth)
s.router.Get("/readyz", s.handleReady)
s.router.Get("/metrics", s.handleMetrics)
s.router.Route("/v1", func(r chi.Router) {
r.Use(s.authMiddleware)
// ...
})
}
Error Response Format (SES-like)
package httpx
type ErrorBody struct {
Error struct {
Type string `json:"Type"` // "Sender" or "Receiver"
Code string `json:"Code"`
Message string `json:"Message"`
Details any `json:"Details,omitempty"`
} `json:"Error"`
RequestId string `json:"RequestId"`
}
func WriteError(w http.ResponseWriter, r *http.Request, status int, code, msg string) {
typ := "Sender"
if status >= 500 {
typ = "Receiver"
}
body := ErrorBody{RequestId: middleware.GetReqID(r.Context())}
body.Error.Type = typ
body.Error.Code = code
body.Error.Message = msg
writeJSON(w, status, body)
}
func WriteSuccess(w http.ResponseWriter, r *http.Request, status int, data map[string]any) {
data["RequestId"] = middleware.GetReqID(r.Context())
writeJSON(w, status, data)
}
Logging
Use log/slog (stdlib, Go 1.21+). JSON em producao, text em dev.
func newLogger(level string) *slog.Logger {
var lvl slog.Level
_ = lvl.UnmarshalText([]byte(level))
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl})
return slog.New(h)
}
Sempre incluir request_id, tenant_id nos logs de request.
ID Generation
package ids
import (
"crypto/rand"
"encoding/hex"
)
func New(prefix string) string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return prefix + "_" + hex.EncodeToString(b)
}
Graceful Shutdown
Todo servico:
- Responde
/healthz(liveness - sempre 200 se processo esta up) - Responde
/readyz(readiness - 200 so quando deps (DB, NATS) estao OK) - Trata SIGINT/SIGTERM com shutdown de 15s
- Drena conexoes HTTP antes de morrer
Testing
internal/...tem tests com_test.goao lado- Integration tests em
tests/usam DB real (dockertest ou testcontainers) - Minimum coverage: 70% pra service logic, 50% pra handlers
Dockerfile Multi-stage
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
Makefile
.PHONY: run test migrate build
run:
go run ./cmd/server
test:
go test ./... -race
migrate-up:
migrate -path ./internal/db/migrations -database "$(DATABASE_URL)" up
migrate-down:
migrate -path ./internal/db/migrations -database "$(DATABASE_URL)" down 1
build:
CGO_ENABLED=0 go build -o bin/server ./cmd/server
docker:
docker build -t asender-<service> .
Endpoints comuns a todos os servicos
GET /healthz- liveness, sempre 200GET /readyz- readiness, 200 se DB + deps OKGET /metrics- Prometheus metricsGET /- retorna{service, version, commit}
Service-to-Service Auth
Shared secret JWT.
// asender-shared/auth/svctoken.go
type SvcClaims struct {
Svc string `json:"svc"`
jwt.RegisteredClaims
}
func SignSvcToken(secret []byte, svc string) (string, error) {
claims := SvcClaims{
Svc: svc,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return tok.SignedString(secret)
}
func VerifySvcToken(secret []byte, tokenStr string) (*SvcClaims, error) {
var claims SvcClaims
_, err := jwt.ParseWithClaims(tokenStr, &claims, func(_ *jwt.Token) (any, error) {
return secret, nil
})
return &claims, err
}
Requests internas setam X-Asender-Svc-Token: <jwt>. Middleware valida.
Frontend Patterns (Next.js)
- App Router (Next 15)
- TypeScript strict
- Tailwind + shadcn/ui
- TanStack Query para API calls client-side
- Server actions + API routes proxyam pro backend
- Env vars:
NEXT_PUBLIC_API_URL,API_URL(server-only) - Auth via httpOnly cookie com JWT
Estrutura:
asender-<frontend>/
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ └── register/
│ ├── (app)/
│ │ └── dashboard/
│ ├── api/ # Route handlers (proxy)
│ └── layout.tsx
├── components/
│ └── ui/ # shadcn
├── lib/
│ ├── api.ts # typed client
│ └── auth.ts
├── public/
├── package.json
├── tsconfig.json
├── tailwind.config.ts
└── next.config.ts