BFF con Go (Parte 2): Tests, Concurrencia y Resiliencia en APIs
Mar 29, 2025 10 min
BFF con Go (Parte 2): Tests, Concurrencia y Resiliencia en APIs 🧪⚙️
En la primera parte, construimos un BFF minimalista en Go usando OpenAPI, con estructura limpia y endpoints claros. En esta segunda entrega, vamos un paso más allá:
- ✅ Añadiremos tests
- 🔁 Implementaremos concurrencia con goroutines y channels
- 🧱 Incorporaremos resiliencia frente a errores de servicios remotos
Todo manteniendo buenas prácticas y un diseño que pueda escalar con tu arquitectura hexagonal o basada en dependencias.
🧪 1. Agregando Tests con httptest
Creamos un test para nuestro handler principal:
func TestGetUserById(t *testing.T) {
r := chi.NewRouter()
h := &handlers.UserHandler{}
api.RegisterHandlers(r, h)
req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("esperado 200, obtenido %d", w.Code)
}
var user api.User
json.NewDecoder(w.Body).Decode(&user)
if user.Id != "123" {
t.Errorf("ID esperado '123', obtenido %s", user.Id)
}
}
🔁 2. Concurrencia con Goroutines y Channels
Supongamos que el BFF necesita llamar a dos servicios remotos al mismo tiempo:
type RemoteUser struct {
Profile api.User
Score int
}
func FetchCombinedData(ctx context.Context, id string) (*RemoteUser, error) {
chUser := make(chan api.User)
chScore := make(chan int)
errCh := make(chan error, 2)
go func() {
user, err := fetchUserService(id)
if err != nil {
errCh <- err
return
}
chUser <- user
}()
go func() {
score, err := fetchScoreService(id)
if err != nil {
errCh <- err
return
}
chScore <- score
}()
var user api.User
var score int
for i := 0; i < 2; i++ {
select {
case u := <-chUser:
user = u
case s := <-chScore:
score = s
case err := <-errCh:
return nil, err
}
}
return &RemoteUser{Profile: user, Score: score}, nil
}
🧱 3. Manejo de Resiliencia (Timeouts + Retry)
Usamos contexto y un patrón básico de retry con backoff:
func fetchUserService(id string) (api.User, error) {
var user api.User
for i := 0; i < 3; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "http://user-service/users/"+id, nil)
res, err := http.DefaultClient.Do(req)
if err == nil && res.StatusCode == http.StatusOK {
json.NewDecoder(res.Body).Decode(&user)
return user, nil
}
time.Sleep(time.Duration(i+1) * 500 * time.Millisecond)
}
return user, fmt.Errorf("falló la conexión con user-service")
}
📦 ¿Y la arquitectura hexagonal?
Estas prácticas encajan perfecto si separas:
- handlers/ ➝ Capa de entrada (HTTP)
- services/ ➝ Capa de lógica de negocio
- clients/ ➝ Acceso a servicios remotos
Puedes usar wire
o fx
para inyección de dependencias. Esto permite testear cada capa por separado sin acoplamientos fuertes.
🧩 Próximos pasos
- 📄 Agregar Swagger UI embebido
- 🔐 Incluir autenticación JWT
- 📦 Dockerizar el proyecto para despliegue en la nube
¿Te gustó esta segunda parte? Comenta o escribeme para incluir una tercera parte. 🚀
~devjaime