Backend-разработка

Построение высокопроизводительного REST API на Go с Fiber: маршрутизация, middleware и деплой в Docker

Ruslan Ismailov Опубликовано 14 мин чтения
П

1. Введение — почему Fiber в 2026 году

Go Fiber REST API сегодня — один из самых популярных стеков для построения высоконагруженных сервисов. Fiber построен поверх fasthttp — самого быстрого HTTP-движка для Go, — что даёт ему преимущество перед net/http в сценариях с тысячами одновременных соединений. По данным актуальных бенчмарков TechEmpower, Fiber стабильно входит в топ-5 фреймворков по метрикам JSON-сериализации и plaintext-запросов.

Почему выбирают Fiber framework 2026:

  • Синтаксис, знакомый Express.js-разработчикам — минимальный порог входа.
  • Встроенные middleware: логирование, CORS, rate limiter, recovery.
  • Активная экосистема: адаптеры для Redis, WebSocket, SSE, gRPC.
  • Нулевые аллокации на горячем пути благодаря fasthttp.

2. Архитектура проекта

Перед написанием кода важно определить структуру папок. Мы используем подход clean architecture lite — без избыточных абстракций, но с чётким разделением слоёв:

myapi/
├── cmd/
│   └── server/
│       └── main.go          # точка входа
├── internal/
│   ├── config/              # конфиг из ENV
│   ├── handler/             # HTTP-обработчики
│   ├── middleware/          # кастомные middleware
│   ├── repository/          # слой работы с БД
│   ├── service/             # бизнес-логика
│   └── model/               # структуры данных
├── pkg/
│   ├── database/            # инициализация pgx и Redis
│   └── validator/           # хелперы валидации
├── migrations/              # SQL-миграции
├── Dockerfile
├── docker-compose.yml
└── .env.example

Слои взаимодействуют строго сверху вниз: handler → service → repository. Middleware инжектируются в маршрутизатор до обработчиков.

3. Определение маршрутов и групп: версионирование API

Правильное версионирование — залог долгосрочной поддержки. Используем префиксы /api/v1 и /api/v2 через группы 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)
    }

    // Защищённые маршруты v1
    protected := v1.Group("/admin", middleware.JWTAuth())
    protected.Get("/stats", h.GetStats)
}

Группы позволяют применять middleware точечно: JWTAuth навешивается только на /admin, не затрагивая публичные эндпоинты.

4. Реализация middleware: логирование, JWT, rate limiting

4.1 Логирование запросов

// 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 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 запросов
        Expiration: time.Minute,  // в минуту с одного 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. Подключение PostgreSQL через pgx и Redis для кэширования

5.1 Инициализация 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 Кэширование ответов с 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 кэширует GET-ответы на заданный TTL
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
    }
}

Кэш-миддлвар читает ответ из Redis по ключу cache:{URL}. При промахе выполняет обработчик и сохраняет тело ответа с заданным TTL — типичный паттерн cache-aside для Go API PostgreSQL Redis.

6. Валидация входных данных и обработка ошибок

Для валидации используем библиотеку go-playground/validator. Создадим единый хелпер:

// 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
}

Пример использования в хендлере 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. Написание тестов для handler-ов

Fiber поддерживает тестирование через метод app.Test(), что позволяет обходиться без реального сетевого соединения:

// 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)
}

Мокируем сервисный слой через testify/mock, изолируя хендлер от базы данных. Это ускоряет прогон тестов и делает их детерминированными.

8. Контейнеризация: многоэтапный Dockerfile и 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"]

Многоэтапная сборка уменьшает итоговый образ с ~300 МБ до ~10–15 МБ. Флаги -s -w убирают отладочную информацию, дополнительно сжимая бинарник.

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:

Условие condition: service_healthy гарантирует, что API-контейнер стартует только после успешных healthcheck-ов PostgreSQL и Redis.

9. Основы CI/CD: автосборка и публикация образа

Настроим простой GitHub Actions pipeline, который при пуше в main запускает тесты, собирает образ и пушит его в 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 }}

Джоб build-and-push выполняется только если тесты прошли, что исключает публикацию сломанного образа. Тегируем образ SHA коммита для возможности отката.

10. Заключение

Мы построили полноценный высокопроизводительный API на Go с использованием Fiber: от маршрутизации с версионированием до кэширования через Redis, контейнеризации с Docker и автоматического CI/CD-пайплайна. Ключевые выводы:

  • Fiber обеспечивает минимальные задержки за счёт fasthttp и zero-alloc подхода.
  • Группы маршрутов и точечное применение middleware делают код масштабируемым.
  • pgx + Redis — проверенная связка для Go REST API с кэшированием на уровне ответов.
  • Многоэтапный Dockerfile сокращает размер образа на порядок, улучшая безопасность и скорость деплоя.
  • GitHub Actions автоматизирует тестирование и публикацию без ручного вмешательства.

Следующий шаг — добавить distributed tracing (OpenTelemetry), горизонтальное масштабирование через Kubernetes и миграции через golang-migrate. Но уже описанная архитектура выдержит десятки тысяч запросов в секунду на скромном железе.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →