Проектирование бесплатного API с пользовательскими данными: почему я отказался от jsonplaceholder
- суббота, 28 марта 2026 г. в 00:00:04

Я периодически провожу технические интервью и смотрю pet-проекты кандидатов.
И почти всегда вижу одну и ту же картину:
Используется localStorage или заглушки вроде jsonplaceholder.
Я прекрасно понимаю, почему так происходит:
никто не хочет платить за сервер
не хочется поднимать backend ради тестового проекта
раньше можно было использовать бесплатный Heroku, но это уже в прошлом
В итоге почти все проекты выглядят одинаково:
статические данные
фейковые API
загрузка через setTimeout, чтобы "сымитировать сервер"
Мне захотелось упростить этот процесс.
Сделать платформу, где:
есть реальные API
можно использовать свои данные
старт занимает ~1 минуту
не нужно поднимать backend
Без необходимости гуглить "free api" и получать те же самые заглушки.
В итоге получилась следующая структура:
web — Next.js (SSG + SEO)
core — Node.js (Express), отвечает за пользователей и API ключи
api-platform — Node.js сервис с самими API
Все сервисы развёрнуты через Docker и проксируются через Nginx.
Если дать бесплатный API, возникает очевидная проблема:
любой может спамить запросы
нагрузка быстро растёт
можно быстро упереться в лимиты сервера
один пользователь может создать непропорционально высокую нагрузку
Поэтому одной из ключевых задач было:
минимизировать обращения к базе данных и быстро отсеивать невалидные запросы
Я не хотел проверять каждый API key через запрос в базу.
Поэтому сделал ключ самодостаточным.
Пример:
Authorization: PetProjects ppk_v1_1_nonce_signature
Формат ключа:
ppk_version_userId_nonce_signature
Где:
version — версия ключа
userId — идентификатор пользователя
nonce — случайное значение
signature — HMAC-подпись
Процесс разбит на два этапа.
1. Быстрая валидация (без DB)
Сначала ключ проверяется локально:
структура
корректность данных
подпись (HMAC)
Это позволяет сразу отсеять мусорные ключи без обращения к базе.
2. Проверка пользователя
Если ключ валиден — извлекается userId и только после этого идёт обращение к базе.
function validateApiKey(apiKey: string): ApiKeyPayload | null { if (!apiKey.startsWith('ppk_')) return null; const parts = apiKey.split('_'); if (parts.length !== 5) return null; const [, version, userIdRaw, nonce, signature] = parts; const userId = Number(userIdRaw); if (!Number.isInteger(userId)) return null; if (!signature || !/^[a-f0-9]{64}$/.test(signature)) return null; const payload = `${version}.${userId}.${nonce}`; const expectedSignature = crypto.createHmac('sha256', API_KEY_SECRET).update(payload).digest('hex'); if (signature.length !== expectedSignature.length) return null; /** * Используется timing-safe сравнение * * Обычное сравнение строк (===) может быть уязвимо к timing-атакам: * проверка останавливается на первом несовпадении символов, * и по времени ответа можно частично восстановить подпись * * timingSafeEqual сравнивает значения за одинаковое время * и не раскрывает информацию о совпадающих символах */ const isValid = crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex')); if (!isValid) return null; return { version, userId, nonce }; }
Таким образом, невалидные ключи отсекаются без обращения к базе, а валидные требуют только одного запроса с последующим кэшированием.
После успешной проверки пользователь кэшируется.
export const apiKeyCache = new LRUCache<number, CachedUserAuth>({ max: 10000, ttl: 5 * 60 * 1000, });
Это даёт несколько преимуществ:
не нужно обращаться к базе на каждый запрос
уменьшается latency
снижается нагрузка
TTL выбран намеренно коротким.
Если ключ утечёт:
он будет работать ограниченное время
затем потребуется повторная проверка через базу
Это компромисс между безопасностью и производительностью.
У такого подхода есть ограничения:
ключ нельзя мгновенно отозвать без дополнительного механизма
при утечке он остаётся валидным до истечения TTL
требуется аккуратная работа с секретом
В будущем можно добавить blacklist, версионирование ключей или моментальный отзыв ключа.
Для упрощения работы добавлен интерактивный playground:
можно отправлять запросы прямо из браузера
ключ подставляется автоматически после авторизации
генерируются примеры для разных языков
Это позволяет протестировать API без настройки окружения.
Пример запроса
curl -L -X POST 'https://api.pet-projects.io/rest/free' \ -H 'Authorization: PetProjects ppk_v1_1_a1b2c3d4e5_a1b2c3d4e5f6g7h8i10j11' \ -H 'Content-Type: application/json' \ -d "{\"hello\":\"world\"}"
Ответ соответствует формату JSend
{ "status": "success", "data": { "id": 99, "payload": { "hello": "world" }, "createdAt": 1774297745939, "updatedAt": 1774297745939 } }
Ниже пример, как это выглядит на практике:

Основная цель проекта — упростить разработку pet‑проектов, demo и убрать необходимость использовать заглушки.
На практике оказалось, что даже простой бесплатный API требует продуманной архитектуры:
минимизации обращений к базе
защиты от абьюза
баланса между безопасностью и производительностью
Будет интересно посмотреть, как подобная система поведёт себя при росте нагрузки и появлении реальных пользователей.
Если интересно посмотреть реализацию:
https://pet-projects.io/ru/apis