javascript

Проектирование бесплатного API с пользовательскими данными: почему я отказался от jsonplaceholder

  • суббота, 28 марта 2026 г. в 00:00:04
https://habr.com/ru/articles/1015906/

Я периодически провожу технические интервью и смотрю 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
    }
}

Ниже пример, как это выглядит на практике:

Пример интерфейса Playground для todos API
Пример интерфейса Playground для todos API

Заключение

Основная цель проекта — упростить разработку pet‑проектов, demo и убрать необходимость использовать заглушки.

На практике оказалось, что даже простой бесплатный API требует продуманной архитектуры:

  • минимизации обращений к базе

  • защиты от абьюза

  • баланса между безопасностью и производительностью

Будет интересно посмотреть, как подобная система поведёт себя при росте нагрузки и появлении реальных пользователей.

Список API

Если интересно посмотреть реализацию:

https://pet-projects.io/ru/apis