
Я периодически провожу технические интервью и смотрю 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, возникает очевидная проблема:
-
любой может спамить запросы
-
нагрузка быстро растёт
-
можно быстро упереться в лимиты сервера
-
один пользователь может создать непропорционально высокую нагрузку
Поэтому одной из ключевых задач было:
минимизировать обращения к базе данных и быстро отсеивать невалидные запросы
Авторизация и API key
Я не хотел проверять каждый 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 = 5 минут
TTL выбран намеренно коротким.
Если ключ утечёт:
-
он будет работать ограниченное время
-
затем потребуется повторная проверка через базу
Это компромисс между безопасностью и производительностью.
Компромиссы решения
У такого подхода есть ограничения:
-
ключ нельзя мгновенно отозвать без дополнительного механизма
-
при утечке он остаётся валидным до истечения TTL
-
требуется аккуратная работа с секретом
В будущем можно добавить blacklist, версионирование ключей или моментальный отзыв ключа.
Playground
Для упрощения работы добавлен интерактивный 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 требует продуманной архитектуры:
-
минимизации обращений к базе
-
защиты от абьюза
-
баланса между безопасностью и производительностью
Будет интересно посмотреть, как подобная система поведёт себя при росте нагрузки и появлении реальных пользователей.
Список API
Если интересно посмотреть реализацию:
https://pet-projects.io/ru/apis
Автор: lamj_io
