Разработка API для доступа к собранным данным
Собрать данные — это половина задачи. Вторая половина — сделать их доступными в формате, который не тормозит клиентов, не ложится под нагрузкой и не рассказывает всем подряд что вы парсили последние полгода. API для blockchain/crypto данных имеет специфику: данные immutable и append-only (исторические записи не меняются), объёмы большие (миллионы транзакций), запросы часто time-series по природе, клиенты хотят как REST для простых запросов так и WebSocket для real-time.
Дизайн REST API
Версионирование и URL структура
/v1/funding-rates/{symbol}?exchange=binance&from=2024-01-01&to=2024-03-01&limit=500
/v1/transactions/{chain}/{address}?from_block=19000000&limit=100
/v1/news?tags=bitcoin,regulation&from=2024-01-15T00:00:00Z&limit=50
/v1/gas/history?network=ethereum&granularity=1h&from=2024-01-01
Принципы:
-
limit+cursorпагинация вместоpage+offset— стабильна при вставке новых данных - Временные параметры в ISO 8601 (включая timezone) или unix milliseconds — принимать оба
-
?fields=для field selection — не отдавать 30 полей если клиент использует 3
Cursor-based пагинация
interface PaginatedResponse<T> {
data: T[];
pagination: {
cursor: string | null; // null = последняя страница
hasMore: boolean;
total?: number; // опционально, дорого считать
};
}
// Cursor = base64(JSON({lastId, lastTimestamp}))
// Запрос следующей страницы: GET /v1/funding-rates?cursor={opaque_string}
При cursor-пагинации добавление новых записей в начало не ломает навигацию клиента по истории.
Query параметры и валидация
Zod для валидации на входе — описывает схему и конвертирует типы:
import { z } from "zod";
const FundingRatesQuerySchema = z.object({
symbol: z.string().regex(/^[A-Z]+-[A-Z]+$/, "Invalid symbol format"),
exchange: z.enum(["binance", "bybit", "okx", "hyperliquid"]).optional(),
from: z.coerce.date(),
to: z.coerce.date(),
limit: z.coerce.number().min(1).max(1000).default(100),
cursor: z.string().optional(),
});
type FundingRatesQuery = z.infer<typeof FundingRatesQuerySchema>;
Ранний возврат 400 с детальными ошибками экономит время клиентов:
{
"error": "VALIDATION_ERROR",
"details": [
{ "field": "from", "message": "Invalid date format" },
{ "field": "limit", "message": "Must be between 1 and 1000" }
]
}
Производительность: кеширование и оптимизация запросов
Многоуровневый кеш
Client → CDN (статичные исторические данные, TTL 1h)
→ Application cache (Redis, TTL 30s–5m)
→ Read replica PostgreSQL / ClickHouse
Redis кеширование по ключу запроса:
async function getFundingRates(query: FundingRatesQuery): Promise<FundingRateRecord[]> {
const cacheKey = `fr:${query.symbol}:${query.exchange ?? "all"}:${query.from.getTime()}:${query.to.getTime()}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const data = await db.queryFundingRates(query);
// Исторические данные (прошлое) кешируем долго, real-time - кратко
const ttl = query.to < new Date(Date.now() - 3600_000) ? 3600 : 30;
await redis.setEx(cacheKey, ttl, JSON.stringify(data));
return data;
}
Database query optimization
Для time-series данных в PostgreSQL — индексы критичны:
-- Покрывающий индекс для типичного запроса
CREATE INDEX CONCURRENTLY idx_funding_rates_lookup
ON funding_rates (symbol, exchange, settled_at DESC)
INCLUDE (funding_rate, mark_price);
-- Запрос должен использовать этот индекс:
EXPLAIN ANALYZE
SELECT settled_at, funding_rate, mark_price
FROM funding_rates
WHERE symbol = 'BTC-USDT'
AND exchange = 'binance'
AND settled_at BETWEEN $1 AND $2
ORDER BY settled_at DESC
LIMIT 100;
Для аналитических запросов (агрегации, средние по периодам) — ClickHouse значительно быстрее PostgreSQL.
WebSocket для real-time данных
// Fastify + @fastify/websocket
fastify.get("/v1/stream", { websocket: true }, (socket, req) => {
const subscriptions = parseSubscriptions(req.query);
const unsubscribers = subscriptions.map((sub) =>
eventBus.on(sub.channel, (data) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ channel: sub.channel, data }));
}
})
);
socket.on("message", (msg) => {
const cmd = JSON.parse(msg.toString());
if (cmd.type === "subscribe") { /* ... */ }
if (cmd.type === "unsubscribe") { /* ... */ }
if (cmd.type === "ping") socket.send(JSON.stringify({ type: "pong" }));
});
socket.on("close", () => unsubscribers.forEach(unsub => unsub()));
});
Heartbeat: сервер шлёт ping каждые 30 секунд, клиент должен ответить pong. Если нет ответа за 10 секунд — закрываем соединение. Это отсекает зависшие соединения которые занимают memory.
Rate limiting и auth
API keys вместо JWT для server-to-server: не истекают, легко инвалидировать, не требуют refresh flow:
// Middleware
async function apiKeyAuth(req: FastifyRequest, reply: FastifyReply) {
const key = req.headers["x-api-key"];
if (!key) return reply.code(401).send({ error: "API key required" });
const apiKey = await redis.hGetAll(`apikey:${key}`);
if (!apiKey.id) return reply.code(401).send({ error: "Invalid API key" });
req.apiKeyId = apiKey.id;
req.rateLimitTier = apiKey.tier; // "free", "standard", "premium"
}
Sliding window rate limiting через Redis Lua script — атомарная операция:
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call("ZREMRANGEBYSCORE", key, 0, now - window)
local count = redis.call("ZCARD", key)
if count >= limit then return 0 end
redis.call("ZADD", key, now, now)
redis.call("EXPIRE", key, window / 1000)
return 1
Rate limit headers в ответах: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset — стандартная практика, клиенты могут адаптировать своё поведение.
Observability
Structured logging каждого запроса с: api_key_id, endpoint, query_params (без секретов), response_time_ms, status_code, cache_hit.
Prometheus метрики:
const httpRequestDuration = new Histogram({
name: "http_request_duration_ms",
help: "HTTP request duration in milliseconds",
labelNames: ["method", "route", "status_code"],
buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500],
});
P99 latency по каждому endpoint — главный SLA метрик. Alert если P99 > 500ms для cached запросов или > 2000ms для некешированных.
Стек: Fastify (Node.js) для REST + WebSocket, Redis для кеша и rate limiting, PostgreSQL/ClickHouse для данных, Prometheus + Grafana для мониторинга.
Срок разработки API поверх готовой БД с данными: 4–7 недель включая auth, rate limiting, документацию (OpenAPI spec).







