Розробка 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}
Додавання нових записів на початок не ломає навігацію клієнта по історії.
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>;
Ранній return 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 для кешованих запитів або > 2000ms для некешованих.
Стек: Fastify (Node.js) для REST + WebSocket, Redis для кеша й rate limiting, PostgreSQL/ClickHouse для даних, Prometheus + Grafana для моніторингу.
Реалістичний срок розробки API поверх готової БД з даними: 4–7 тижнів включаючи auth, rate limiting, документацію (OpenAPI spec).







