Статьи
AI-статья 16 мин 765

FastAPI vs Go Fiber vs Node Fastify: бенчмарк API на 10K rps

Сравнение трёх фреймворков для JSON API: FastAPI, Go Fiber, Fastify. Бенчмарки с PostgreSQL, middleware, тестирование, Docker. Когда какой выбирать.

Эта статья сгенерирована AI-моделью и может содержать неточности. Проверяйте информацию перед применением в production.

Три фреймворка, одна задача: быстрый API

Когда нужно построить API-сервис — не монолит с ORM и шаблонами, а чистый JSON API на 10-50 эндпоинтов с минимальной latency — выбор стека определяет всё: от производительности до стоимости инфраструктуры.

Три фреймворка борются за этот сегмент: FastAPI (Python) — с автодокументацией и типами, Go Fiber — с производительностью на уровне C, Node Fastify — с экосистемой npm и async/await. Каждый обещает скорость. Каждый решает задачу по-разному.

Я работал с FastAPI на ML-пайплайне (inference API для моделей), Go Fiber на микросервисе авторизации (50K+ rps), Fastify на BFF-слое (backend-for-frontend, агрегация 5 микросервисов). Вот что я вынес из каждого.

Архитектура и философия

FastAPI — типизация и документация из коробки

FastAPI построен на Starlette (ASGI) и Pydantic. Главная идея: описываешь типы — получаешь валидацию, сериализацию и документацию автоматически.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from datetime import datetime

app = FastAPI(title="User API", version="1.0.0")

class UserCreate(BaseModel):
    name: str
    email: EmailStr
    age: int | None = None

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    created_at: datetime

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
    # Pydantic уже валидировал входные данные
    # email проверен, age — опциональный int
    db_user = await db.users.insert(user.model_dump())
    return UserResponse(**db_user)

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    user = await db.users.find_one(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return UserResponse(**user)

Запустил — и на /docs уже есть Swagger UI с интерактивной документацией. На /redoc — ReDoc. Типы из Pydantic автоматически превращаются в JSON Schema.

Go Fiber — минимализм и скорость

Fiber вдохновлён Express.js, но написан на Go с использованием fasthttp (быстрее стандартного net/http в 5-10 раз).

package main

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

type UserCreate struct {
    Name  string `json:"name" validate:"required,min=2"`
    Email string `json:"email" validate:"required,email"`
    Age   *int   `json:"age,omitempty" validate:"omitempty,gte=0,lte=150"`
}

type UserResponse struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

func main() {
    app := fiber.New(fiber.Config{
        Prefork:       true,  // multi-process (как nginx workers)
        CaseSensitive: true,
        StrictRouting:  true,
    })

    app.Use(logger.New())
    app.Use(recover.New())

    app.Post("/users", createUser)
    app.Get("/users/:id", getUser)

    app.Listen(":3000")
}

func createUser(c *fiber.Ctx) error {
    var input UserCreate
    if err := c.BodyParser(&input); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid JSON"})
    }

    // Валидация через go-playground/validator
    if err := validate.Struct(input); err != nil {
        return c.Status(422).JSON(fiber.Map{"error": err.Error()})
    }

    user := db.CreateUser(input)
    return c.Status(201).JSON(UserResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     user.Email,
        CreatedAt: user.CreatedAt,
    })
}

func getUser(c *fiber.Ctx) error {
    id, err := c.ParamsInt("id")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid ID"})
    }

    user, err := db.FindUser(id)
    if err != nil {
        return c.Status(404).JSON(fiber.Map{"error": "User not found"})
    }

    return c.JSON(UserResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     user.Email,
        CreatedAt: user.CreatedAt,
    })
}

Нет автодокументации из коробки (нужен swagger-gen или swaggo). Нет автовалидации (нужен validator). Но: горутины вместо event loop, скомпилированный бинарник, минимальный overhead.

Node Fastify — структурированный Express

Fastify — «фреймворк для тех, кому Express слишком хаотичен». Schema-based валидация через JSON Schema, плагинная архитектура, встроенный логгер (Pino).

import Fastify from 'fastify';

const app = Fastify({ logger: true });

// JSON Schema для валидации и документации
const createUserSchema = {
  body: {
    type: 'object',
    required: ['name', 'email'],
    properties: {
      name: { type: 'string', minLength: 2 },
      email: { type: 'string', format: 'email' },
      age: { type: 'integer', minimum: 0, maximum: 150 },
    },
  },
  response: {
    201: {
      type: 'object',
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' },
        email: { type: 'string' },
        created_at: { type: 'string', format: 'date-time' },
      },
    },
  },
};

app.post('/users', { schema: createUserSchema }, async (request, reply) => {
  const { name, email, age } = request.body as any;

  const user = await db.users.create({ name, email, age });

  reply.code(201).send({
    id: user.id,
    name: user.name,
    email: user.email,
    created_at: user.created_at,
  });
});

app.get('/users/:id', async (request, reply) => {
  const { id } = request.params as { id: string };
  const user = await db.users.findById(parseInt(id));

  if (!user) {
    reply.code(404).send({ error: 'User not found' });
    return;
  }

  reply.send(user);
});

app.listen({ port: 3000, host: '0.0.0.0' });

Fastify использует JSON Schema для валидации (Ajv) и сериализации (fast-json-stringify). Схема описывает и входные данные, и формат ответа — Fastify генерирует оптимизированный сериализатор.

Сравнение архитектуры

Аспект FastAPI Go Fiber Node Fastify
Язык Python 3.10+ Go 1.21+ TypeScript/JS
Runtime uvicorn (ASGI) fasthttp (Go) Node.js (V8)
Конкурентность async/await (asyncio) Горутины (green threads) Event loop + async/await
Валидация Pydantic (встроенная) go-playground/validator JSON Schema (Ajv)
Документация Swagger UI (встроенная) swaggo (сторонний) @fastify/swagger
Типизация Python type hints Go structs TypeScript interfaces
Middleware ASGI middleware Fiber middleware Fastify hooks + plugins
Экосистема PyPI (500K+ пакетов) Go modules npm (2M+ пакетов)

CRUD API: полный пример

FastAPI

from fastapi import FastAPI, HTTPException, Query, Depends
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
import databases
import sqlalchemy

DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/mydb"
database = databases.Database(DATABASE_URL)

app = FastAPI()

# Модели
class ProductCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    description: str | None = None
    price: float = Field(..., gt=0)
    category: str
    in_stock: bool = True

class ProductUpdate(BaseModel):
    name: str | None = None
    description: str | None = None
    price: float | None = Field(None, gt=0)
    category: str | None = None
    in_stock: bool | None = None

class ProductResponse(BaseModel):
    id: int
    name: str
    description: str | None
    price: float
    category: str
    in_stock: bool
    created_at: datetime
    updated_at: datetime

class PaginatedResponse(BaseModel):
    items: list[ProductResponse]
    total: int
    page: int
    per_page: int

# Эндпоинты
@app.get("/products", response_model=PaginatedResponse)
async def list_products(
    page: int = Query(1, ge=1),
    per_page: int = Query(20, ge=1, le=100),
    category: str | None = None,
    min_price: float | None = None,
    max_price: float | None = None,
    search: str | None = None,
):
    query = products_table.select()

    if category:
        query = query.where(products_table.c.category == category)
    if min_price is not None:
        query = query.where(products_table.c.price >= min_price)
    if max_price is not None:
        query = query.where(products_table.c.price <= max_price)
    if search:
        query = query.where(products_table.c.name.ilike(f"%{search}%"))

    total = await database.fetch_val(
        query.with_only_columns(sqlalchemy.func.count())
    )
    rows = await database.fetch_all(
        query.offset((page - 1) * per_page).limit(per_page)
    )

    return PaginatedResponse(
        items=[ProductResponse(**dict(r)) for r in rows],
        total=total,
        page=page,
        per_page=per_page,
    )

@app.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(product: ProductCreate):
    now = datetime.utcnow()
    query = products_table.insert().values(
        **product.model_dump(), created_at=now, updated_at=now
    )
    product_id = await database.execute(query)
    return ProductResponse(id=product_id, **product.model_dump(),
                           created_at=now, updated_at=now)

@app.patch("/products/{product_id}", response_model=ProductResponse)
async def update_product(product_id: int, product: ProductUpdate):
    updates = product.model_dump(exclude_unset=True)
    if not updates:
        raise HTTPException(400, "No fields to update")
    updates["updated_at"] = datetime.utcnow()
    await database.execute(
        products_table.update()
        .where(products_table.c.id == product_id)
        .values(**updates)
    )
    row = await database.fetch_one(
        products_table.select().where(products_table.c.id == product_id)
    )
    if not row:
        raise HTTPException(404, "Product not found")
    return ProductResponse(**dict(row))

@app.delete("/products/{product_id}", status_code=204)
async def delete_product(product_id: int):
    result = await database.execute(
        products_table.delete().where(products_table.c.id == product_id)
    )
    if not result:
        raise HTTPException(404, "Product not found")

@app.on_event("startup")
async def startup():
    await database.connect()

@app.on_event("shutdown")
async def shutdown():
    await database.disconnect()

Go Fiber

// handlers/products.go
func ListProducts(c *fiber.Ctx) error {
    page := c.QueryInt("page", 1)
    perPage := c.QueryInt("per_page", 20)
    category := c.Query("category")
    search := c.Query("search")

    query := db.Model(&Product{})

    if category != "" {
        query = query.Where("category = ?", category)
    }
    if search != "" {
        query = query.Where("name ILIKE ?", "%"+search+"%")
    }

    var total int64
    query.Count(&total)

    var products []Product
    query.Offset((page - 1) * perPage).Limit(perPage).Find(&products)

    return c.JSON(fiber.Map{
        "items":    products,
        "total":    total,
        "page":     page,
        "per_page": perPage,
    })
}

func CreateProduct(c *fiber.Ctx) error {
    var input ProductCreate
    if err := c.BodyParser(&input); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid JSON"})
    }
    if err := validate.Struct(input); err != nil {
        return c.Status(422).JSON(fiber.Map{"error": formatValidationErrors(err)})
    }

    product := Product{
        Name:        input.Name,
        Description: input.Description,
        Price:       input.Price,
        Category:    input.Category,
        InStock:     input.InStock,
    }
    result := db.Create(&product)
    if result.Error != nil {
        return c.Status(500).JSON(fiber.Map{"error": "Failed to create"})
    }

    return c.Status(201).JSON(product)
}

func UpdateProduct(c *fiber.Ctx) error {
    id, _ := c.ParamsInt("id")

    var product Product
    if err := db.First(&product, id).Error; err != nil {
        return c.Status(404).JSON(fiber.Map{"error": "Not found"})
    }

    var input map[string]interface{}
    if err := c.BodyParser(&input); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid JSON"})
    }

    db.Model(&product).Updates(input)
    return c.JSON(product)
}

func DeleteProduct(c *fiber.Ctx) error {
    id, _ := c.ParamsInt("id")
    result := db.Delete(&Product{}, id)
    if result.RowsAffected == 0 {
        return c.Status(404).JSON(fiber.Map{"error": "Not found"})
    }
    return c.SendStatus(204)
}

Node Fastify

import Fastify from 'fastify';
import { PrismaClient } from '@prisma/client';

const app = Fastify({ logger: true });
const prisma = new PrismaClient();

// Schemas
const productSchema = {
  type: 'object',
  properties: {
    id: { type: 'integer' },
    name: { type: 'string' },
    description: { type: 'string', nullable: true },
    price: { type: 'number' },
    category: { type: 'string' },
    in_stock: { type: 'boolean' },
    created_at: { type: 'string', format: 'date-time' },
  },
};

// List
app.get('/products', {
  schema: {
    querystring: {
      type: 'object',
      properties: {
        page: { type: 'integer', default: 1, minimum: 1 },
        per_page: { type: 'integer', default: 20, minimum: 1, maximum: 100 },
        category: { type: 'string' },
        search: { type: 'string' },
      },
    },
  },
}, async (request) => {
  const { page, per_page, category, search } = request.query as any;

  const where: any = {};
  if (category) where.category = category;
  if (search) where.name = { contains: search, mode: 'insensitive' };

  const [items, total] = await Promise.all([
    prisma.product.findMany({
      where,
      skip: (page - 1) * per_page,
      take: per_page,
      orderBy: { created_at: 'desc' },
    }),
    prisma.product.count({ where }),
  ]);

  return { items, total, page, per_page };
});

// Create
app.post('/products', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'price', 'category'],
      properties: {
        name: { type: 'string', minLength: 1 },
        description: { type: 'string' },
        price: { type: 'number', exclusiveMinimum: 0 },
        category: { type: 'string' },
        in_stock: { type: 'boolean', default: true },
      },
    },
    response: { 201: productSchema },
  },
}, async (request, reply) => {
  const product = await prisma.product.create({
    data: request.body as any,
  });
  reply.code(201).send(product);
});

// Update
app.patch('/products/:id', async (request, reply) => {
  const { id } = request.params as { id: string };
  try {
    const product = await prisma.product.update({
      where: { id: parseInt(id) },
      data: request.body as any,
    });
    reply.send(product);
  } catch {
    reply.code(404).send({ error: 'Not found' });
  }
});

// Delete
app.delete('/products/:id', async (request, reply) => {
  const { id } = request.params as { id: string };
  try {
    await prisma.product.delete({ where: { id: parseInt(id) } });
    reply.code(204).send();
  } catch {
    reply.code(404).send({ error: 'Not found' });
  }
});

app.listen({ port: 3000, host: '0.0.0.0' });

Бенчмарки

Условия

  • Машина: 4 ядра (AMD Ryzen 7), 16 GB RAM, Ubuntu 24.04
  • PostgreSQL 16 с 1M записей в таблице products
  • HTTP load: wrk, 8 потоков, 256 соединений, 30 секунд
  • Каждый фреймворк в Docker, single process (кроме Fiber с Prefork)

JSON response (без базы, статический JSON)

Метрика FastAPI Go Fiber Node Fastify
Requests/sec 18,500 185,000 62,000
Latency p50 12ms 1.2ms 3.5ms
Latency p99 45ms 4ms 12ms
Memory 85 MB 12 MB 65 MB

Go Fiber в 10 раз быстрее FastAPI на чистом JSON. Fastify — в 3 раза быстрее FastAPI. Но это синтетика — в реальности bottleneck всегда база данных.

CRUD с PostgreSQL (SELECT + JOIN)

Метрика FastAPI Go Fiber Node Fastify
Requests/sec 4,200 12,800 8,500
Latency p50 55ms 18ms 28ms
Latency p99 180ms 45ms 85ms
Memory 120 MB 25 MB 95 MB

С базой данных разрыв сокращается: Go Fiber в 3 раза быстрее FastAPI, Fastify — в 2 раза. Bottleneck перемещается на PostgreSQL.

Тяжёлый запрос (агрегация + внешний API call)

Метрика FastAPI Go Fiber Node Fastify
Requests/sec 850 2,100 1,800
Latency p50 280ms 115ms 135ms
Latency p99 900ms 350ms 420ms

На I/O-bound задачах Fastify подтягивается к Go благодаря event loop. FastAPI теряет из-за GIL и overhead asyncio.

Startup time

Метрика FastAPI Go Fiber Node Fastify
Cold start 1.2s 0.05s 0.8s
Docker image 150 MB 12 MB 80 MB

Go — мгновенный старт. Критично для serverless (Lambda, Cloud Functions), где cold start = деньги.

Middleware и плагины

FastAPI: Dependency Injection

from fastapi import Depends, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Security(security),
    db: Database = Depends(get_db),
) -> User:
    token = credentials.credentials
    payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    user = await db.users.find_one(payload["user_id"])
    if not user:
        raise HTTPException(401, "Invalid token")
    return user

async def require_admin(user: User = Depends(get_current_user)) -> User:
    if user.role != "admin":
        raise HTTPException(403, "Admin only")
    return user

@app.get("/admin/users")
async def admin_list_users(admin: User = Depends(require_admin)):
    return await db.users.find_all()

FastAPI DI — одна из лучших реализаций в любом фреймворке. Зависимости цепляются, кэшируются, типизируются. Тестирование — через app.dependency_overrides.

Go Fiber: Middleware chain

func AuthMiddleware() fiber.Handler {
    return func(c *fiber.Ctx) error {
        token := c.Get("Authorization")
        if token == "" {
            return c.Status(401).JSON(fiber.Map{"error": "No token"})
        }

        claims, err := jwt.Parse(strings.TrimPrefix(token, "Bearer "))
        if err != nil {
            return c.Status(401).JSON(fiber.Map{"error": "Invalid token"})
        }

        c.Locals("user_id", claims.UserID)
        return c.Next()
    }
}

func AdminOnly() fiber.Handler {
    return func(c *fiber.Ctx) error {
        userID := c.Locals("user_id").(int)
        user, _ := db.FindUser(userID)
        if user.Role != "admin" {
            return c.Status(403).JSON(fiber.Map{"error": "Admin only"})
        }
        c.Locals("user", user)
        return c.Next()
    }
}

// Применение
admin := app.Group("/admin", AuthMiddleware(), AdminOnly())
admin.Get("/users", listUsers)

Node Fastify: Hooks и Decorators

// Auth plugin
app.decorate('authenticate', async (request: FastifyRequest, reply: FastifyReply) => {
  const token = request.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    reply.code(401).send({ error: 'No token' });
    return;
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!);
    request.user = payload as UserPayload;
  } catch {
    reply.code(401).send({ error: 'Invalid token' });
  }
});

// Route-level hook
app.get('/admin/users', {
  preHandler: [app.authenticate],
}, async (request) => {
  if (request.user.role !== 'admin') {
    throw { statusCode: 403, message: 'Admin only' };
  }
  return prisma.user.findMany();
});

Тестирование

FastAPI: TestClient

from fastapi.testclient import TestClient
import pytest

@pytest.fixture
def client():
    app.dependency_overrides[get_db] = lambda: test_db
    with TestClient(app) as client:
        yield client
    app.dependency_overrides.clear()

def test_create_product(client):
    response = client.post("/products", json={
        "name": "Test Product",
        "price": 29.99,
        "category": "test",
    })
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Test Product"
    assert data["price"] == 29.99

def test_create_product_invalid(client):
    response = client.post("/products", json={
        "name": "",  # too short
        "price": -5,  # negative
    })
    assert response.status_code == 422

Go Fiber: httptest

func TestCreateProduct(t *testing.T) {
    app := setupTestApp()

    body := `{"name":"Test","price":29.99,"category":"test"}`
    req := httptest.NewRequest("POST", "/products", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    resp, err := app.Test(req, -1)
    assert.NoError(t, err)
    assert.Equal(t, 201, resp.StatusCode)

    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    assert.Equal(t, "Test", result["name"])
}

Node Fastify: inject

import { test } from 'node:test';
import assert from 'node:assert';

test('POST /products creates product', async () => {
  const response = await app.inject({
    method: 'POST',
    url: '/products',
    payload: { name: 'Test', price: 29.99, category: 'test' },
  });

  assert.strictEqual(response.statusCode, 201);
  const data = response.json();
  assert.strictEqual(data.name, 'Test');
});

Fastify inject() — запрос без сети, самый быстрый способ тестирования HTTP. FastAPI TestClient — тоже in-process. Go app.Test() — аналогично.

Деплой и Docker

FastAPI

FROM python:3.12-slim AS builder
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
COPY --from=builder /install /usr/local
COPY . /app
WORKDIR /app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Go Fiber

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /server

FROM scratch
COPY --from=builder /server /server
CMD ["/server"]

Node Fastify

FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && cp -R node_modules /prod_modules && npm ci

FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=deps /prod_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

Когда что выбирать

FastAPI — когда

  • ML/AI inference API: интеграция с PyTorch, TensorFlow, scikit-learn
  • Data-heavy API: pandas, numpy, scipy в pipeline
  • Быстрое прототипирование: автодокументация экономит дни
  • Команда знает Python лучше других языков
  • Нужна OpenAPI-документация для внешних клиентов
  • Нагрузка < 5K rps (для большего — добавить workers или перейти на Go)

Go Fiber — когда

  • High-performance API: > 10K rps, низкая latency критична
  • Microservices: маленький Docker-образ, мгновенный cold start
  • Serverless (Lambda): cold start 50ms vs 1200ms у Python
  • Инфраструктурные сервисы: proxy, gateway, auth service
  • Команда знает Go или готова учить
  • Нужна предсказуемая производительность без GC-пауз

Node Fastify — когда

  • BFF (Backend-for-Frontend): агрегация нескольких API
  • Real-time: WebSocket + HTTP в одном процессе
  • Fullstack TypeScript: общие типы между фронтом и бэком
  • npm-экосистема критична: конкретная библиотека есть только в npm
  • Команда фронтендеров пишет бэкенд
  • Нагрузка 5-15K rps: быстрее FastAPI, проще Go

Ни один из трёх

  • Монолит с ORM, миграциями, шаблонами → Rails, Laravel, Django
  • Enterprise с CQRS, DDD → Spring Boot, .NET
  • Embedded/системное программирование → Rust (Actix, Axum)

Итоговый чеклист

Выбор

  • [ ] Определить основную нагрузку: CPU-bound → Go, I/O-bound → Fastify/FastAPI, ML → FastAPI
  • [ ] Определить target rps: < 5K → любой, 5-15K → Fastify/Go, > 15K → Go
  • [ ] Определить экосистему: PyPI (data/ML), npm (frontend libs), Go modules (infra)
  • [ ] Оценить cold start requirements: serverless → Go, containers → любой

Архитектура

  • [ ] Валидация на уровне фреймворка: Pydantic / JSON Schema / go-validator
  • [ ] Структурированные ошибки: единый формат {"error": ..., "code": ...}
  • [ ] Health check endpoint: GET /health с проверкой зависимостей
  • [ ] Graceful shutdown: обработка SIGTERM

Production

  • [ ] Rate limiting: встроенный или через nginx
  • [ ] CORS: настроить для конкретных доменов
  • [ ] Structured logging: JSON-логи для сборщика (ELK, Loki)
  • [ ] Metrics: Prometheus endpoint /metrics
  • [ ] Tracing: OpenTelemetry для distributed tracing
  • [ ] Docker: multi-stage build, non-root user

Комментарии (0)