О насБлогКонтакты
Backend18 апреля 2012 г. 4 мин 23

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

AunimedaAunimeda
📋 Содержание

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

Весной 2012 года мы делали первый самостоятельный REST API для Android-приложения. До этого у нас были только веб-сайты — форм-сабмит, серверный рендеринг, сессии в куках. API для мобильного клиента был другим миром.

Мы прочли Roy Fielding's диссертацию (ну, её реферат). Прочли несколько статей на Хабре. И начали делать. Вот что вышло.


Ошибка №1: URL как имена процедур

Первая версия нашего API выглядела так:

GET  /api/getUser?id=123
POST /api/createOrder
GET  /api/getUserOrders?userId=123&status=active
POST /api/cancelOrder?orderId=456
GET  /api/getProductList?categoryId=2&page=1

Технически работало. Но это RPC через HTTP, не REST. Проблема проявилась через месяц: у нас было 40 endpoint'ов, все начинались с глагола, никакой структуры. Новый разработчик не мог предсказать URL — только смотреть в документацию.

Переписали по принципу «ресурсы, а не действия»:

GET    /api/v1/users/123              → получить пользователя
GET    /api/v1/users/123/orders       → заказы пользователя
GET    /api/v1/orders?status=active   → список заказов с фильтром
POST   /api/v1/orders                 → создать заказ
DELETE /api/v1/orders/456             → отменить заказ (не GET!)
GET    /api/v1/products?category=2    → список продуктов

Глаголы — HTTP-методы. Существительные — в URL. Новый разработчик мог угадать структуру.


Ошибка №2: API без версионирования

Первый месяц мы жили без версий в URL. Когда Android-клиент уже был у тестеров, нам потребовалось изменить структуру ответа /api/orders — добавить поле и переименовать существующее.

Мы изменили. Старый клиент сломался у тестеров. Нам повезло — приложение ещё не было в Play Store.

Версионирование через URL (/api/v1/) — самый простой вариант:

// routes.php — маршрутизация по версии
$version = $router->getVersion(); // Извлекает 'v1' из URL

switch ($version) {
    case 'v1':
        require 'controllers/v1/' . $controller . '.php';
        break;
    case 'v2':
        require 'controllers/v2/' . $controller . '.php';
        break;
    default:
        jsonResponse(400, ['error' => 'Unknown API version']);
}

Правило, которое мы приняли: v1 никогда не ломается. Новые фичи — в v2. Deprecated endpoint'ы в v1 живут ещё 6 месяцев, потом удаляются с предупреждением в заголовке.

// Deprecated endpoint: предупреждаем, но не ломаем
header('X-Deprecated: This endpoint will be removed on 2013-01-01. Use /v2/orders instead.');
// Возвращаем старую структуру ответа как обычно

Ошибка №3: токены в GET-параметрах

Первая аутентификация выглядела так:

GET /api/v1/orders?user_id=123&token=abc123def456

Проблема: токен в URL попадает в:

  • Логи сервера (access.log)
  • Историю браузера
  • Referrer-заголовок при переходе на другой сайт
  • Кэши прокси-серверов

Мы читали о HTTPS и думали, что это защищает. HTTPS шифрует передачу, но не мешает попаданию URL в логи на сервере.

Правильно: токен в заголовке Authorization:

GET /api/v1/orders
Authorization: Bearer abc123def456
// Извлечение токена из заголовка
function getAuthToken(): ?string {
    $header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
    if (preg_match('/^Bearer\s+(.+)$/i', $header, $m)) {
        return $m[1];
    }
    return null;
}

function authenticate(): ?array {
    $token = getAuthToken();
    if (!$token) {
        jsonResponse(401, ['error' => 'Authorization required']);
        exit;
    }
    
    $user = User::findByToken($token);
    if (!$user || $user->token_expires_at < time()) {
        jsonResponse(401, ['error' => 'Invalid or expired token']);
        exit;
    }
    
    return $user;
}

Ошибка №4: HTTP 200 на всё подряд

Первое время мы возвращали HTTP 200 с полем success: false при ошибках. Это заставляло клиент парсить тело ответа, чтобы понять — успех это или ошибка:

// Плохо: 200 OK с признаком ошибки внутри
HTTP/1.1 200 OK
{"success": false, "error": "User not found"}

Правильно использовать семантику HTTP-статусов:

function jsonResponse(int $status, array $data): void {
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

// Использование
if (!$user) {
    jsonResponse(404, ['error' => 'User not found', 'code' => 'USER_NOT_FOUND']);
}
if (!$user->canAccessOrder($orderId)) {
    jsonResponse(403, ['error' => 'Access denied', 'code' => 'FORBIDDEN']);
}
if (!$validator->passes()) {
    jsonResponse(422, ['error' => 'Validation failed', 'fields' => $validator->errors()]);
}

// Успех
jsonResponse(200, ['data' => $order]);
// Создание ресурса
jsonResponse(201, ['data' => $newOrder, 'id' => $newOrder->id]);

Клиент на Android мог теперь проверить response.code() и обрабатывать ошибки без парсинга JSON:

if (!response.isSuccessful()) {
    // 4xx — ошибка клиента, показать пользователю
    // 5xx — ошибка сервера, показать "попробуйте позже"
    handleError(response.code());
    return;
}
// 2xx — обработать успешный ответ

Что получилось в итоге

Через три месяца и две итерации у нас был API, которым мы не стыдились:

  • URL-структура на основе ресурсов
  • Версионирование с /v1/ в пути
  • JWT-подобные токены в заголовке Authorization
  • Корректные HTTP-статусы
  • Единый формат ошибки {"error": "...", "code": "..."}
  • Документация в Markdown — по одной странице на endpoint

Первая версия API — это всегда обучение в бою. Важно не то, что вы сделали ошибки с версионированием и токенами. Важно что вы их заметили, пока приложение было у тестеров, а не у 50,000 пользователей с разными версиями клиента, которые все сломались одновременно.

Читайте также

Как масштабировать Laravel приложение на VPS когда начинает тормозить (2015)aunimeda
Backend

Как масштабировать Laravel приложение на VPS когда начинает тормозить (2015)

Laravel из коробки с 1000+ одновременными пользователями начинает тормозить. В 2015 году типичный стек масштабирования: PHP-FPM с правильными worker pools, Redis для сессий и кэша, очереди для тяжёлых задач, supervisor для workers. Без переписывания кода — только конфигурация.

Разработка приложения для доставки еды в Самареaunimeda
Разработка приложений

Разработка приложения для доставки еды в Самаре

Как создать собственное приложение доставки для самарского рынка: архитектура, интеграция с ЮKassa и СБП, геолокация курьеров и бюджет запуска.

Разработка приложения такси в Самаре: архитектура и бюджет 2026aunimeda
Разработка приложений

Разработка приложения такси в Самаре: архитектура и бюджет 2026

Как создать приложение-такси для самарского рынка: алгоритм матчинга, геолокация, интеграция СБП и ЮKassa, конкуренция с Яндекс Go. Реальный бюджет запуска.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в России. Бесплатная консультация.

Получить консультацию Все статьи