Desarrollo backend

Construcción de una REST API de alto rendimiento en Go con Fiber: enrutamiento, middleware y despliegue en Docker

Ruslan Ismailov Publicado 14 min de lectura
C

1. Introducción — por qué Fiber en 2026

Go Fiber REST API es hoy uno de los stacks más populares para construir servicios de alta carga. Fiber está construido sobre fasthttp —el motor HTTP más rápido para Go—, lo que le otorga ventaja sobre net/http en escenarios con miles de conexiones simultáneas. Según los benchmarks actuales de TechEmpower, Fiber se sitúa de forma constante en el top 5 de frameworks en métricas de serialización JSON y peticiones plaintext.

Por qué elegir Fiber framework 2026:

  • Sintaxis familiar para desarrolladores de Express.js — curva de aprendizaje mínima.
  • Middleware integrados: logging, CORS, rate limiter, recovery.
  • Ecosistema activo: adaptadores para Redis, WebSocket, SSE, gRPC.
  • Cero alocaciones en el camino crítico gracias a fasthttp.

2. Arquitectura del proyecto

Antes de escribir código, es importante definir la estructura de carpetas. Utilizamos el enfoque clean architecture lite — sin abstracciones excesivas, pero con una separación clara de capas:

myapi/
├── cmd/
│   └── server/
│       └── main.go          # punto de entrada
├── internal/
│   ├── config/              # configuración desde ENV
│   ├── handler/             # manejadores HTTP
│   ├── middleware/          # middleware personalizados
│   ├── repository/          # capa de acceso a BD
│   ├── service/             # lógica de negocio
│   └── model/               # estructuras de datos
├── pkg/
│   ├── database/            # inicialización de pgx y Redis
│   └── validator/           # helpers de validación
├── migrations/              # migraciones SQL
├── Dockerfile
├── docker-compose.yml
└── .env.example

Las capas interactúan estrictamente de arriba hacia abajo: handler → service → repository. Los middleware se inyectan en el enrutador antes que los manejadores.

3. Definición de rutas y grupos: versionado de la API

El versionado correcto es la clave para el soporte a largo plazo. Usamos prefijos /api/v1 y /api/v2 mediante grupos de Fiber:

// internal/handler/routes.go
package handler

import (
    "github.com/gofiber/fiber/v2"
    "myapi/internal/middleware"
)

func RegisterRoutes(app *fiber.App, h *UserHandler) {
    api := app.Group("/api")

    v1 := api.Group("/v1", middleware.Logger())
    {
        users := v1.Group("/users")
        users.Get("/",       h.ListUsers)
        users.Post("/",      h.CreateUser)
        users.Get("/:id",    h.GetUser)
        users.Put("/:id",    h.UpdateUser)
        users.Delete("/:id", h.DeleteUser)
    }

    // Rutas protegidas v1
    protected := v1.Group("/admin", middleware.JWTAuth())
    protected.Get("/stats", h.GetStats)
}

Los grupos permiten aplicar middleware de forma selectiva: JWTAuth solo se aplica en /admin, sin afectar los endpoints públicos.

4. Implementación de middleware: logging, JWT y rate limiting

4.1 Logging de peticiones

// internal/middleware/logger.go
package middleware

import (
    "github.com/gofiber/fiber/v2"
    fiberLogger "github.com/gofiber/fiber/v2/middleware/logger"
)

func Logger() fiber.Handler {
    return fiberLogger.New(fiberLogger.Config{
        Format: "[${time}] ${status} - ${latency} ${method} ${path}\n",
    })
}

4.2 Autenticación JWT

// internal/middleware/jwt.go
package middleware

import (
    "github.com/gofiber/fiber/v2"
    jwtware "github.com/gofiber/contrib/jwt"
    "os"
)

func JWTAuth() fiber.Handler {
    return jwtware.New(jwtware.Config{
        SigningKey: jwtware.SigningKey{
            Key: []byte(os.Getenv("JWT_SECRET")),
        },
        ErrorHandler: func(c *fiber.Ctx, err error) error {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error": "invalid or expired token",
            })
        },
    })
}

4.3 Rate Limiting

// internal/middleware/ratelimit.go
package middleware

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/limiter"
    "time"
)

func RateLimiter() fiber.Handler {
    return limiter.New(limiter.Config{
        Max:        100,          // 100 peticiones
        Expiration: time.Minute,  // por minuto desde una IP
        KeyGenerator: func(c *fiber.Ctx) string {
            return c.IP()
        },
        LimitReached: func(c *fiber.Ctx) error {
            return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
                "error": "rate limit exceeded",
            })
        },
    })
}

5. Conexión con PostgreSQL mediante pgx y Redis para caché

5.1 Inicialización de PostgreSQL (pgx)

// pkg/database/postgres.go
package database

import (
    "context"
    "fmt"
    "os"

    "github.com/jackc/pgx/v5/pgxpool"
)

func NewPostgresPool(ctx context.Context) (*pgxpool.Pool, error) {
    dsn := fmt.Sprintf(
        "postgres://%s:%s@%s:%s/%s?sslmode=disable",
        os.Getenv("POSTGRES_USER"),
        os.Getenv("POSTGRES_PASSWORD"),
        os.Getenv("POSTGRES_HOST"),
        os.Getenv("POSTGRES_PORT"),
        os.Getenv("POSTGRES_DB"),
    )
    pool, err := pgxpool.New(ctx, dsn)
    if err != nil {
        return nil, fmt.Errorf("pgxpool.New: %w", err)
    }
    if err = pool.Ping(ctx); err != nil {
        return nil, fmt.Errorf("postgres ping: %w", err)
    }
    return pool, nil
}

5.2 Caché de respuestas con Redis

// pkg/database/redis.go
package database

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/redis/go-redis/v9"
)

func NewRedisClient() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:     fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")),
        Password: os.Getenv("REDIS_PASSWORD"),
        DB:       0,
    })
}

// CacheMiddleware almacena en caché las respuestas GET con un TTL dado
func CacheResponse(rdb *redis.Client, ttl time.Duration) fiber.Handler {
    return func(c *fiber.Ctx) error {
        if c.Method() != "GET" {
            return c.Next()
        }
        key := "cache:" + c.OriginalURL()
        cached, err := rdb.Get(context.Background(), key).Bytes()
        if err == nil {
            c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8)
            return c.Status(fiber.StatusOK).Send(cached)
        }
        if err := c.Next(); err != nil {
            return err
        }
        rdb.Set(context.Background(), key, c.Response().Body(), ttl)
        return nil
    }
}

El middleware de caché lee la respuesta de Redis con la clave cache:{URL}. Si no hay caché, ejecuta el manejador y almacena el cuerpo de la respuesta con el TTL indicado — patrón típico de cache-aside para Go API PostgreSQL Redis.

6. Validación de datos de entrada y manejo de errores

Para la validación usamos la librería go-playground/validator. Crearemos un helper centralizado:

// pkg/validator/validator.go
package validator

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

var validate = validator.New()

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

func Validate(s any) []ValidationError {
    var errors []ValidationError
    err := validate.Struct(s)
    if err == nil {
        return nil
    }
    for _, e := range err.(validator.ValidationErrors) {
        errors = append(errors, ValidationError{
            Field:   e.Field(),
            Message: fmt.Sprintf("failed on tag '%s'", e.Tag()),
        })
    }
    return errors
}

Ejemplo de uso en el handler CreateUser:

// internal/handler/user.go
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var req model.CreateUserRequest
    if err := c.BodyParser(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "cannot parse body",
        })
    }
    if errs := validator.Validate(req); errs != nil {
        return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
            "errors": errs,
        })
    }
    user, err := h.svc.CreateUser(c.Context(), req)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    return c.Status(fiber.StatusCreated).JSON(user)
}

7. Escritura de pruebas para los handlers

Fiber soporta pruebas a través del método app.Test(), lo que permite prescindir de una conexión de red real:

// internal/handler/user_test.go
package handler_test

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gofiber/fiber/v2"
    "github.com/stretchr/testify/assert"
    "myapi/internal/handler"
    "myapi/internal/service/mocks"
)

func TestCreateUser_Success(t *testing.T) {
    mockSvc := new(mocks.UserService)
    mockSvc.On("CreateUser", mock.Anything, mock.AnythingOfType("model.CreateUserRequest")).
        Return(&model.User{ID: 1, Email: "test@example.com"}, nil)

    h := handler.NewUserHandler(mockSvc)
    app := fiber.New()
    app.Post("/users", h.CreateUser)

    body, _ := json.Marshal(map[string]string{
        "email":    "test@example.com",
        "password": "secret123",
    })
    req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    resp, err := app.Test(req, -1)
    assert.NoError(t, err)
    assert.Equal(t, http.StatusCreated, resp.StatusCode)
    mockSvc.AssertExpectations(t)
}

Simulamos la capa de servicio mediante testify/mock, aislando el handler de la base de datos. Esto acelera la ejecución de las pruebas y las hace deterministas.

8. Contenedorización: Dockerfile multietapa y Docker Compose

8.1 Dockerfile

# Dockerfile

# ---- Build stage ----
FROM golang:1.23-alpine AS builder
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server ./cmd/server

# ---- Run stage ----
FROM gcr.io/distroless/static-debian12
WORKDIR /app

COPY --from=builder /app/server .
COPY --from=builder /app/migrations ./migrations

EXPOSE 3000
USER nonroot:nonroot
ENTRYPOINT ["/app/server"]

La construcción multietapa reduce el tamaño final de la imagen de ~300 MB a ~10–15 MB. Los flags -s -w eliminan la información de depuración, comprimiendo adicionalmente el binario.

8.2 docker-compose.yml

# docker-compose.yml
version: "3.9"

services:
  api:
    build: .
    container_name: myapi
    ports:
      - "3000:3000"
    env_file: .env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    container_name: myapi_postgres
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: myapi_redis
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

La condición condition: service_healthy garantiza que el contenedor de la API solo arranque tras los healthchecks exitosos de PostgreSQL y Redis.

9. Fundamentos de CI/CD: construcción automática y publicación de la imagen

Configuraremos un pipeline simple de GitHub Actions que, al hacer push a main, ejecuta las pruebas, construye la imagen y la publica en Docker Hub:

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.23"
      - name: Run tests
        run: go test ./... -race -coverprofile=coverage.out
      - name: Upload coverage
        uses: codecov/codecov-action@v4

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ secrets.DOCKER_USERNAME }}/myapi:latest
            ${{ secrets.DOCKER_USERNAME }}/myapi:${{ github.sha }}

El job build-and-push solo se ejecuta si las pruebas han pasado, evitando la publicación de una imagen defectuosa. La imagen se etiqueta con el SHA del commit para permitir la vuelta atrás.

10. Conclusión

Hemos construido una API de alto rendimiento en Go completa con Fiber: desde el enrutamiento con versionado hasta el caché con Redis, la contenedorización con Docker y un pipeline CI/CD automatizado. Conclusiones clave:

  • Fiber garantiza latencias mínimas gracias a fasthttp y el enfoque zero-alloc.
  • Los grupos de rutas y la aplicación selectiva de middleware hacen el código escalable.
  • pgx + Redis es una combinación probada para REST APIs en Go con caché a nivel de respuesta.
  • El Dockerfile multietapa reduce el tamaño de la imagen en un orden de magnitud, mejorando la seguridad y la velocidad de despliegue.
  • GitHub Actions automatiza las pruebas y la publicación sin intervención manual.

El siguiente paso sería agregar distributed tracing (OpenTelemetry), escalado horizontal con Kubernetes y migraciones mediante golang-migrate. Pero la arquitectura descrita ya es capaz de soportar decenas de miles de peticiones por segundo en hardware modesto.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →