Layering Handler-Servicio-Repositorio
El patrón handler-service-repository divide los adaptadores HTTP (o gRPC), la lógica de negocio y la persistencia en tres capas testeables.
Busca en todas las páginas de la documentación
El patrón handler-service-repository divide los adaptadores HTTP (o gRPC), la lógica de negocio y la persistencia en tres capas testeables.
Es la arquitectura por defecto para APIs en Go que superan main.go con SQL en línea.
Los handlers traducen formatos de red y códigos de estado.
Los services aplican reglas y orquestan flujos de trabajo.
Los repositories se comunican con bases de datos, cachés y APIs externas.
Las dependencias fluyen handler → service → interfaz de repository, nunca al revés.
Cuándo usar esto: Al construir servicios REST o gRPC, al incorporar desarrolladores backend, o al extraer lógica de handlers "gordos".
Tarjeta de referencia rápida - lista para copiar y pegar.
// puerto del repositorio
type UserRepo interface {
ByEmail(ctx context.Context, email string) (User, error)
Insert(ctx context.Context, u User) error
}
// servicio
func (s *Service) Register(ctx context.Context, email string) error { /* reglas */ }
// handler
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { /* decodificar, llamar al servicio, responder */ }Cuándo usar esto:
if de lógica de negocio más largas que una pantalla.httptest y 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))
}Lo que esto demuestra:
Service usan un Repository falso sin HTTP ni SQL.Handler mapea errores de dominio a códigos de estado HTTP en un solo lugar.PG es la única capa que conoce los nombres de las columnas SQL.Cliente -> Handler -> Service -> Repository -> Base de Datos
<- <- <-
El contexto se propaga para la cancelación.
Las transacciones generalmente comienzan en el servicio (o en un puerto UnitOfWork) y pasan *sql.Tx a los repositorios cuando es necesario.
| HTTP | Equivalente gRPC |
|---|---|
Handler | Método grpc.Server en un servicio generado |
| Decodificación JSON | Des-serialización proto |
http.Error | status.Error(codes.*, msg) |
Mantén los services idénticos; solo cambian los adaptadores de entrada.
// Repositorio falso para pruebas de servicio
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) con tipos Handler, Service y PG en lugar de tres paquetes para APIs pequeñas.sql.Rows a los handlers - Expone la lógica de escaneo en la capa HTTP. Solución: Los repositories devuelven structs o slices tipados.Service con cuarenta métodos. Solución: Divide por agregado (UserService, BillingService).ctx context.Context en todas las I/O.| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Handlers "gordos" | Scripts de administración, prototipos | APIs de producto con reglas en evolución |
| Puertos/adaptadores hexagonales | Muchos sistemas de I/O y puntos de entrada | CRUD simple con una base de datos |
| ORM Active Record | El equipo prioriza la velocidad | Invariantes de dominio complejas |
| División de lectura/escritura CQRS | Los modelos de lectura divergen mucho | CRUD directo |
Comienza en un paquete de características (internal/user).
Divide cuando la claridad o las reglas de importación requieran subpaquetes internal/user/http.
El middleware HTTP envuelve a los handlers; pasa el principal a través de valores de contexto o campos explícitos del handler.
Los services aplican reglas de autorización con puertos Authorizer inyectados.
Devuelve un ErrNotFound de dominio desde el repositorio o deja que el servicio mapee sql.ErrNoRows.
Sé consistente en todos los repositorios.
El servicio inicia la transacción, pasa un wrapper Tx que implementa Repository a los helpers, y confirma en caso de éxito.
Evita que los handlers abran transacciones.
Sí, para orquestación, pero ten cuidado con los ciclos.
Prefiere componer puertos de nivel inferior o un servicio fachada.
Los métodos del repositorio aceptan structs Page; los services aplican valores por defecto y límites.
Los handlers analizan los parámetros de consulta en Page.
No.
net/http ServeMux (Go 1.22+) es suficiente; los routers son solo preocupaciones de borde.
httptest con services falsos inyectados en Handler.
Aserciona códigos de estado y cuerpos JSON para la lógica de mapeo.
DTOs generados o escritos a mano en el paquete adaptador del handler.
Mapea a dominio antes de llamar al servicio.
Sí - reemplaza el handler HTTP con un adaptador de consumidor de mensajes que llame al mismo servicio.
Versiones de Stack: Esta página fue escrita para Go 1.26.x (predeterminado GC de Green Tea, modernizadores de
go fix- verificar parche en la compilación), chi (última versión - verificar en la compilación), gin (última versión - verificar en la compilación), echo (última versión - verificar en la compilación), google.golang.org/grpc (última versión - verificar en la compilación), sigs.k8s.io/controller-runtime (última versión - verificar en la compilación), kubebuilder (última versión - verificar en la compilación), tinygo (última versión - verificar objetivos de placa en la compilación), wazero (última versión - verificar en la compilación) y golangci-lint (última versión - verificar conjunto de linters en la compilación).
Revisado por Chris St. John·Última actualización: 18 jul 2026