Camadas Handler-Service-Repository
O padrão handler-service-repository divide adaptadores HTTP (ou gRPC), lógica de negócios e persistência em três camadas testáveis.
Busque em todas as páginas da documentação
O padrão handler-service-repository divide adaptadores HTTP (ou gRPC), lógica de negócios e persistência em três camadas testáveis.
É a arquitetura padrão para APIs Go que superam main.go com SQL inline.
Handlers traduzem formatos de comunicação e códigos de status.
Services aplicam regras e orquestram fluxos de trabalho.
Repositories se comunicam com bancos de dados, caches e APIs externas.
As dependências fluem handler → service → interface do repository, nunca o inverso.
Quando usar isso: Construindo serviços REST ou gRPC, integrando desenvolvedores backend, ou extraindo lógica de handlers "gordos".
Cartão de receita de referência rápida - pronto para copiar e colar.
// porta do repository
type UserRepo interface {
ByEmail(ctx context.Context, email string) (User, error)
Insert(ctx context.Context, u User) error
}
// service
func (s *Service) Register(ctx context.Context, email string) error { /* regras */ }
// handler
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { /* decodificar, chamar service, responder */ }Quando usar isso:
if de lógica de negócios mais longas que uma tela.httptest e Postgres.// internal/user/repo.go
package user
import (
"context"
"time"
)
type User struct {
ID int64
Email string
CreatedAt time.Time
}
type Repository interface {
FindByEmail(ctx context.Context, email string) (User, error)
Create(ctx context.Context, u User) error
}// internal/user/service.go
package user
import (
"context"
"errors"
"fmt"
"strings"
"time"
)
var (
ErrInvalidEmail = errors.New("user: invalid email")
ErrExists = errors.New("user: email already registered")
)
type Service struct {
repo Repository
now func() time.Time
}
func NewService(repo Repository) *Service {
return &Service{repo: repo, now: time.Now}
}
func (s *Service) Register(ctx context.Context, email string) error {
email = strings.TrimSpace(strings.ToLower(email))
if !strings.Contains(email, "@") {
return ErrInvalidEmail
}
if _, err := s.repo.FindByEmail(ctx, email); err == nil {
return ErrExists
}
u := User{Email: email, CreatedAt: s.now()}
if err := s.repo.Create(ctx, u); err != nil {
return fmt.Errorf("register: %w", err)
}
return nil
}// internal/user/postgres.go
package user
import (
"context"
"database/sql"
"errors"
)
type PG struct {
db *sql.DB
}
func (p *PG) FindByEmail(ctx context.Context, email string) (User, error) {
var u User
err := p.db.QueryRowContext(ctx,
`SELECT id, email, created_at FROM users WHERE email=$1`, email,
).Scan(&u.ID, &u.Email, &u.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return User{}, err
}
return u, err
}
func (p *PG) Create(ctx context.Context, u User) error {
_, err := p.db.ExecContext(ctx,
`INSERT INTO users (email, created_at) VALUES ($1,$2)`, u.Email, u.CreatedAt,
)
return err
}// internal/user/http.go
package user
import (
"encoding/json"
"errors"
"net/http"
)
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
var in struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
err := h.svc.Register(r.Context(), in.Email)
if err != nil {
switch {
case errors.Is(err, ErrInvalidEmail):
http.Error(w, err.Error(), http.StatusBadRequest)
case errors.Is(err, ErrExists):
http.Error(w, err.Error(), http.StatusConflict)
default:
http.Error(w, "server error", http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusCreated)
}// cmd/api/main.go
package main
import (
"database/sql"
"log"
"net/http"
_ "github.com/jackc/pgx/v5/stdlib"
"example.com/app/internal/user"
)
func main() {
db, err := sql.Open("pgx", "postgres://localhost/app?sslmode=disable")
if err != nil {
log.Fatal(err)
}
repo := &user.PG{db: db}
svc := user.NewService(repo)
h := user.NewHandler(svc)
mux := http.NewServeMux()
mux.HandleFunc("POST /users", h.Register)
log.Fatal(http.ListenAndServe(":8080", mux))
}O que isso demonstra:
Service usam um Repository falso sem HTTP ou SQL.Handler mapeia erros de domínio para códigos de status HTTP em um único local.PG é a única camada que conhece os nomes das colunas SQL.Client -> Handler -> Service -> Repository -> Database
<- <- <-
O contexto propaga para cancelamento.
Transações geralmente começam no serviço (ou em uma porta UnitOfWork) e passam *sql.Tx para os repositórios quando necessário.
| HTTP | Equivalente gRPC |
|---|---|
Handler | Método grpc.Server em um serviço gerado |
| Decodificação JSON | Deserialização proto |
http.Error | status.Error(codes.*, msg) |
Mantenha os serviços idênticos; apenas os adaptadores de condução mudam.
// Repositório falso para testes de serviço
type memRepo struct {
byEmail map[string]User
}
func (m *memRepo) FindByEmail(ctx context.Context, email string) (User, error) {
u, ok := m.byEmail[email]
if !ok {
return User{}, errors.New("not found")
}
return u, nil
}user) com tipos Handler, Service e PG em vez de três pacotes para APIs pequenas.sql.Rows para handlers - Expõe a lógica de escaneamento na camada HTTP. Correção: Repositories retornam structs ou slices tipados.Service com quarenta métodos. Correção: Divida por agregado (UserService, BillingService).ctx context.Context em todas as I/O.| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Handlers "gordos" | Scripts de administração, protótipos | APIs de produto com regras em evolução |
| Portas/adaptadores hexagonais | Muitos sistemas de I/O e pontos de entrada | CRUD simples com um banco de dados |
| ORM Active Record | Equipe prioriza velocidade | Invariantes de domínio complexas |
| Divisão de leitura/escrita CQRS | Modelos de leitura divergem muito | CRUD direto |
Comece em um pacote de funcionalidade (internal/user).
Divida quando a clareza ou as regras de importação exigirem subpacotes internal/user/http.
Middleware HTTP envolve os handlers; passe o principal via valores de contexto ou campos explícitos do handler.
Services aplicam regras de autorização com portas Authorizer injetadas.
Retorne um ErrNotFound de domínio do repository ou deixe o service mapear sql.ErrNoRows.
Seja consistente entre os repositories.
O service inicia a transação, passa o wrapper Tx implementando Repository para os helpers, e confirma em caso de sucesso.
Evite que handlers abram transações.
Sim, para orquestração, mas cuidado com ciclos.
Prefira compor portas de nível inferior ou um facade service.
Métodos do repository aceitam structs Page; services aplicam padrões e limites.
Handlers analisam parâmetros de consulta em Page.
Não.
net/http ServeMux (Go 1.22+) é suficiente; roteadores são apenas preocupações de borda.
httptest com services falsos injetados no Handler.
Afirme códigos de status e corpos JSON para a lógica de mapeamento.
DTOs gerados ou escritos manualmente no pacote adaptador do handler.
Mapeie para o domínio antes de chamar o service.
Sim - substitua o handler HTTP por um adaptador de consumidor de mensagens chamando o mesmo service.
Versões da Stack: Esta página foi escrita para Go 1.26.x (GC padrão Green Tea, go fix modernizers - verifique o patch na compilação), chi (última versão - verifique na compilação), gin (última versão - verifique na compilação), echo (última versão - verifique na compilação), google.golang.org/grpc (última versão - verifique na compilação), sigs.k8s.io/controller-runtime (última versão - verifique na compilação), kubebuilder (última versão - verifique na compilação), tinygo (última versão - verifique os alvos de placa na compilação), wazero (última versão - verifique na compilação) e golangci-lint (última versão - verifique o conjunto de linters na compilação).
Revisado por Chris St. John·Última atualização: 18 de jul. de 2026