Перейти к основному содержимому

API Layer — Слой API

Версия: 1.0
Дата: 19.04.2026
Статус: Черновик

Обзор

API Layer обеспечивает единую точку доступа к системе для всех клиентов согласно разделу "### 5. API layer (Слой API)" в Архитектура платформы. Включает API Gateway на Traefik и Partner API для B2B интеграций. Реализует принцип из Архитектура платформы: "Веб-сайт и внешние партнёры используют одни и те же API. Одна логика для всех."

Архитектура API: См. диаграмму vitrip_api_architecture.jpg — схема API Gateway, REST endpoints, аутентификации и маршрутизации к Business Services.

Архитектура API Layer vitrip.store

Partner API структура: См. диаграмму vitrip_partner_api.jpg — детальная схема B2B API с sandbox/live окружениями, ключами доступа и документацией.

Partner API структура vitrip.store

API Gateway flow: См. диаграмму vitrip_gateway_flow.jpg — поток запросов через Traefik с middleware цепочками, rate limiting и мониторингом.

API Gateway поток запросов vitrip.store

Архитектура компонентов

API Gateway (Traefik)

Назначение: Маршрутизация, аутентификация, rate limiting, логирование согласно Архитектура платформы.

Технология: Traefik согласно технологическому стеку в Архитектура платформы: "API Gateway | Traefik | Маршрутизация, middleware, интеграция с K8s".

Конфигурация Traefik

# traefik.yml
api:
dashboard: true
insecure: false

entryPoints:
web:
address: ":80"
http:
redirections:
entrypoint:
to: websecure
scheme: https
websecure:
address: ":443"

certificatesResolvers:
letsencrypt:
acme:
email: admin@vitrip.store
storage: /acme/acme.json
httpChallenge:
entryPoint: web

providers:
kubernetes:
endpoints:
- https://kubernetes.default.svc:443

metrics:
prometheus:
addRoutersLabels: true

Middleware цепочки

# middleware.yml
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: auth-chain
spec:
chain:
middlewares:
- name: rate-limiter
- name: cors-headers
- name: api-auth
- name: request-logger

---
# Rate limiting
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: rate-limiter
spec:
rateLimit:
burst: 100 # Burst requests
period: 1s # Time window
sourceCriterion:
requestHeaderName: "X-API-Key" # Per API key limiting

REST API Endpoints

Экспонирует gRPC интерфейсы из Business Services как RESTful API для веб-клиентов и партнёров.

Search API

# Базируется на SearchService gRPC из business_services.md
paths:
/api/v1/hotels/search:
post:
summary: "Поиск отелей"
description: "Экспонирует SearchService.SearchHotels() как REST"
requestBody:
content:
application/json:
schema:
type: object
required: [location, dates, guests]
properties:
location:
$ref: '#/components/schemas/GeoLocation'
dates:
$ref: '#/components/schemas/DateRange'
guests:
type: integer
minimum: 1
maximum: 20
priceRange:
$ref: '#/components/schemas/PriceRange'
amenities:
type: array
items:
type: string
sortBy:
type: string
enum: [price_asc, price_desc, rating_desc, distance_asc]
default: rating_desc
page:
type: integer
minimum: 1
default: 1
pageSize:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SearchResponse'

/api/v1/hotels/{hotelId}:
get:
summary: "Детали отеля"
description: "Экспонирует SearchService.GetHotelDetails() как REST"
parameters:
- name: hotelId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/HotelDetails'

/api/v1/hotels/{hotelId}/rooms/available:
get:
summary: "Доступные номера"
description: "Экспонирует SearchService.GetAvailableRooms() как REST"
parameters:
- name: hotelId
in: path
required: true
schema:
type: string
format: uuid
- name: checkIn
in: query
required: true
schema:
type: string
format: date
- name: checkOut
in: query
required: true
schema:
type: string
format: date
- name: guests
in: query
required: true
schema:
type: integer
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/RoomAvailability'

Pricing API

# Базируется на PricingService gRPC из business_services.md
paths:
/api/v1/hotels/{hotelId}/prices:
get:
summary: "Цены отеля"
description: "Экспонирует PricingService.GetHotelPrices() как REST"
parameters:
- name: hotelId
in: path
required: true
schema:
type: string
format: uuid
- name: checkIn
in: query
required: true
schema:
type: string
format: date
- name: checkOut
in: query
required: true
schema:
type: string
format: date
- name: roomType
in: query
required: true
schema:
type: string
- name: currency
in: query
schema:
type: string
pattern: '^[A-Z]{3}$'
default: 'USD'
security:
- ApiKeyAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PriceResponse'

Booking API

# Базируется на BookingService gRPC из business_services.md
paths:
/api/v1/bookings:
post:
summary: "Создать бронирование"
description: "Экспонирует BookingService.CreateBooking() как REST"
requestBody:
content:
application/json:
schema:
type: object
required: [hotelId, roomType, dates, guest, payment]
properties:
hotelId:
type: string
format: uuid
roomType:
type: string
dates:
$ref: '#/components/schemas/DateRange'
guest:
$ref: '#/components/schemas/GuestDetails'
payment:
$ref: '#/components/schemas/PaymentDetails'
security:
- ApiKeyAuth: []
- BearerAuth: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/BookingResponse'
'400':
$ref: '#/components/responses/BadRequest'
'402':
description: "Payment Required"
'409':
description: "Room not available"

/api/v1/bookings/{bookingId}:
get:
summary: "Детали бронирования"
description: "Экспонирует BookingService.GetBookingDetails() как REST"
parameters:
- name: bookingId
in: path
required: true
schema:
type: string
format: uuid
security:
- ApiKeyAuth: []
- BearerAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BookingDetails'

delete:
summary: "Отменить бронирование"
description: "Экспонирует BookingService.CancelBooking() как REST"
parameters:
- name: bookingId
in: path
required: true
schema:
type: string
format: uuid
security:
- ApiKeyAuth: []
- BearerAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/CancellationResponse'

Tour Builder API

# Базируется на TourBuilderService gRPC из business_services.md
paths:
/api/v1/tours:
post:
summary: "Создать программу тура"
description: "Экспонирует TourBuilderService.CreateTourProgram() как REST"
requestBody:
content:
application/json:
schema:
type: object
required: [tourName, dates, destinations]
properties:
tourName:
type: string
maxLength: 200
dates:
$ref: '#/components/schemas/DateRange'
destinations:
type: array
items:
$ref: '#/components/schemas/Destination'
totalGuests:
type: integer
minimum: 1
maximum: 50
security:
- ApiKeyAuth: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/TourResponse'

/api/v1/tours/{tourId}/pdf:
get:
summary: "Генерация PDF программы"
description: "Экспонирует TourBuilderService.GenerateTourPDF() как REST"
parameters:
- name: tourId
in: path
required: true
schema:
type: string
format: uuid
- name: lang
in: query
schema:
type: string
enum: [ru, en, uk]
default: ru
security:
- ApiKeyAuth: []
responses:
'200':
content:
application/pdf:
schema:
type: string
format: binary

Partner API

Ключи доступа и окружения

Согласно разделу "### 5. API layer (Слой API)" в Архитектура платформы: "Partner API — REST API для B2B клиентов с документацией и sandbox".

// Типы ключей доступа
type APIKeyType string

const (
SandboxKey APIKeyType = "sandbox" // sb_1234567890abcdef
ProductionKey APIKeyType = "live" // pk_1234567890abcdef
)

type APIKey struct {
ID string `json:"id"`
PartnerID string `json:"partner_id"`
Type APIKeyType `json:"type"`
Name string `json:"name"`
KeyHash string `json:"key_hash"` // bcrypt hash
Prefix string `json:"prefix"` // sb_ или pk_
RateLimit RateLimit `json:"rate_limit"`
Permissions []string `json:"permissions"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at"`
ExpiresAt *time.Time `json:"expires_at"`
Active bool `json:"active"`
}

type RateLimit struct {
RequestsPerSecond int `json:"requests_per_second"`
RequestsPerHour int `json:"requests_per_hour"`
RequestsPerDay int `json:"requests_per_day"`
}

Окружения

Sandbox Environment

Base URL: https://api-sandbox.vitrip.store/v1
Rate Limits:
- Requests: 100/min, 1000/hour
- No actual bookings (mock responses)
- Test data only
- Free access

Example Key: sb_test_1234567890abcdef123456789012

Production Environment

Base URL: https://api.vitrip.store/v1
Rate Limits:
- Requests: Based on plan (1000-10000/hour)
- Real bookings and payments
- Live hotel inventory
- Commercial terms

Example Key: pk_live_1234567890abcdef123456789012

Authentication & Authorization

// Middleware для проверки API ключей
func (gw *APIGateway) AuthenticateAPIKey(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
apiKey := r.Header.Get("X-API-Key")
if apiKey == "" {
apiKey = r.URL.Query().Get("api_key")
}

if apiKey == "" {
writeError(w, http.StatusUnauthorized, "API key required")
return
}

// Проверка и загрузка ключа из Redis cache
key, err := gw.validateAPIKey(r.Context(), apiKey)
if err != nil {
writeError(w, http.StatusUnauthorized, "Invalid API key")
return
}

// Rate limiting на основе ключа
if err := gw.checkRateLimit(r.Context(), key); err != nil {
writeError(w, http.StatusTooManyRequests, "Rate limit exceeded")
return
}

// Добавляем метаданные в контекст
ctx := context.WithValue(r.Context(), "api_key", key)
ctx = context.WithValue(ctx, "partner_id", key.PartnerID)

next.ServeHTTP(w, r.WithContext(ctx))
})
}

Rate Limiting

// Redis-based sliding window rate limiter
type RateLimiter struct {
redis *redis.Client
window time.Duration
}

func (rl *RateLimiter) Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error) {
now := time.Now().Unix()
pipeline := rl.redis.Pipeline()

// Sliding window counter в Redis
windowKey := fmt.Sprintf("rate:%s:%d", key, now/int64(window.Seconds()))

pipeline.Incr(ctx, windowKey)
pipeline.Expire(ctx, windowKey, window)

results, err := pipeline.Exec(ctx)
if err != nil {
return false, err
}

count := results[0].(*redis.IntCmd).Val()
return count <= int64(limit), nil
}

gRPC ↔ REST Gateway

Трансляция запросов

// gRPC Gateway для трансляции REST → gRPC
type GRPCGateway struct {
searchClient searchpb.SearchServiceClient
pricingClient pricingpb.PricingServiceClient
bookingClient bookingpb.BookingServiceClient
tourBuilderClient tourpb.TourBuilderServiceClient
}

// REST endpoint → gRPC call
func (gw *GRPCGateway) SearchHotels(w http.ResponseWriter, r *http.Request) {
var req SearchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}

// Конвертация REST request → gRPC request
grpcReq := &searchpb.SearchRequest{
Location: &searchpb.GeoLocation{
Lat: req.Location.Lat,
Lng: req.Location.Lng,
},
Dates: &searchpb.DateRange{
CheckIn: req.Dates.CheckIn.Format("2006-01-02"),
CheckOut: req.Dates.CheckOut.Format("2006-01-02"),
},
Guests: int32(req.Guests),
PriceRange: convertPriceRange(req.PriceRange),
Amenities: req.Amenities,
SortBy: req.SortBy,
Page: int32(req.Page),
PageSize: int32(req.PageSize),
}

// Вызов gRPC сервиса (согласно business_services.md)
grpcResp, err := gw.searchClient.SearchHotels(r.Context(), grpcReq)
if err != nil {
writeError(w, http.StatusInternalServerError, "Search service error")
return
}

// Конвертация gRPC response → REST response
resp := convertSearchResponse(grpcResp)
writeJSON(w, http.StatusOK, resp)
}

Мониторинг и наблюдаемость

Prometheus метрики

// API Gateway метрики
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "api_requests_total",
Help: "Total number of API requests",
},
[]string{"method", "endpoint", "status", "partner_id"},
)

httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "api_request_duration_seconds",
Help: "API request duration in seconds",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
},
[]string{"method", "endpoint", "partner_id"},
)

rateLimitExceeded = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "api_rate_limit_exceeded_total",
Help: "Total number of rate limit exceeded errors",
},
[]string{"partner_id", "api_key_type"},
)
)

Трассировка запросов

// OpenTelemetry трассировка через все сервисы
func (gw *GRPCGateway) tracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tracer := otel.Tracer("api-gateway")
ctx, span := tracer.Start(r.Context(), fmt.Sprintf("%s %s", r.Method, r.URL.Path))
defer span.End()

// Передача trace context в gRPC calls
span.SetAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.String()),
attribute.String("partner_id", getPartnerID(r)),
)

next.ServeHTTP(w, r.WithContext(ctx))
})
}

Health Checks

// Health check endpoint для Kubernetes
func (gw *GRPCGateway) healthCheck(w http.ResponseWriter, r *http.Request) {
checks := map[string]string{
"search_service": gw.checkGRPCService("search"),
"pricing_service": gw.checkGRPCService("pricing"),
"booking_service": gw.checkGRPCService("booking"),
"tour_builder_service": gw.checkGRPCService("tour_builder"),
"redis": gw.checkRedis(),
}

overall := "healthy"
for _, status := range checks {
if status != "healthy" {
overall = "degraded"
break
}
}

response := HealthResponse{
Status: overall,
Checks: checks,
Timestamp: time.Now(),
}

statusCode := http.StatusOK
if overall == "degraded" {
statusCode = http.StatusServiceUnavailable
}

writeJSON(w, statusCode, response)
}

Развёртывание и масштабирование

Kubernetes манифесты

# API Gateway Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
labels:
app: api-gateway
spec:
replicas: 3
selector:
matchLabels:
app: api-gateway
template:
metadata:
labels:
app: api-gateway
spec:
containers:
- name: api-gateway
image: vitrip/api-gateway:v1.0
ports:
- containerPort: 8080
name: http
- containerPort: 8081
name: metrics
env:
- name: SEARCH_SERVICE_URL
value: "search-service:80"
- name: PRICING_SERVICE_URL
value: "pricing-service:80"
- name: BOOKING_SERVICE_URL
value: "booking-service:80"
- name: REDIS_URL
value: "redis:6379"
resources:
requests:
memory: "512Mi"
cpu: "300m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5

---
# Traefik Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-gateway-ingress
annotations:
traefik.ingress.kubernetes.io/router.middlewares: "default-auth-chain@kubernetescrd"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- api.vitrip.store
- api-sandbox.vitrip.store
secretName: api-tls-cert
rules:
- host: api.vitrip.store
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
- host: api-sandbox.vitrip.store
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80

Auto-scaling

# HorizontalPodAutoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-gateway-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-gateway
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

Связанная документация

  • Общая архитектура: раздел "### 5. API layer (Слой API)" в Архитектура платформы
  • Business Services: все gRPC интерфейсы из Business Services экспонируются как REST
  • Потоки данных: разделы "### Поток пользовательских запросов" и "### Поток бронирования" в Архитектура платформы
  • Технологический стек: Traefik выбор из таблицы в Архитектура платформы
  • Планируемая документация: детальные OpenAPI спецификации в reference/reference/api-contracts.md