Работа с API: REST, GraphQL и практические примеры

API (Application Programming Interface) — это интерфейс, который позволяет разным приложениям общаться друг с другом. Когда вы заходите на сайт через мобильное приложение, когда платёжная система подтверждает транзакцию, когда бот отправляет сообщение в Telegram — везде работает API. Сегодня умение проектировать, создавать и использовать API — базовый навык для любого веб-разработчика.

В этой статье мы подробно разберём:

  • Что такое API и зачем они нужны.
  • REST — принципы, методы, статус-коды, примеры.
  • Создание простого REST API на PHP.
  • Аутентификацию и авторизацию (Basic, API-ключи, JWT, OAuth2).
  • GraphQL — что это, чем отличается от REST, примеры запросов и мутаций.
  • Сравнение REST и GraphQL: когда что выбирать.
  • Документирование API (Swagger/OpenAPI).
  • Версионирование API.
  • Практические советы и частые ошибки.

1. Что такое API и зачем они нужны

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

Зачем нужны API:

  • Интеграция — связать разные сервисы (сайт, мобильное приложение, CRM).
  • Разделение — отделить фронтенд от бэкенда.
  • Масштабирование — разные команды могут работать над разными частями.
  • Публичный доступ — предоставить данные или функции внешним разработчикам.

Типы API:

  • REST — самый популярный, использует HTTP-методы и URL.
  • GraphQL — язык запросов, позволяет клиенту точно указывать, какие данные нужны.
  • SOAP — устаревший, тяжёлый протокол на основе XML.
  • gRPC — высокопроизводительный, использует бинарный протокол.

2. REST API

REST (Representational State Transfer) — архитектурный стиль, который определяет правила построения API. REST не является стандартом, но большинство современных API следуют его принципам.

2.1 Принципы REST

  1. Клиент-сервер — клиент и сервер разделены, каждый может развиваться независимо.
  2. Отсутствие состояния (Stateless) — каждый запрос содержит всю информацию, необходимую для его обработки. Сервер не хранит состояние клиента между запросами.
  3. Кеширование — ответы могут кешироваться.
  4. Единообразие интерфейса — использование стандартных HTTP-методов и URL.
  5. Слои — клиент может не знать, общается ли он напрямую с сервером или через прокси.
  6. Код по требованию (опционально) — сервер может передавать клиенту исполняемый код.

2.2 HTTP-методы

МетодНазначениеПример
GETПолучить данныеGET /users — список пользователей
POSTСоздать ресурсPOST /users — создать пользователя
PUTПолностью обновить ресурсPUT /users/1 — обновить пользователя с ID 1
PATCHЧастично обновить ресурсPATCH /users/1 — обновить только имя
DELETEУдалить ресурсDELETE /users/1 — удалить пользователя

2.3 Статус-коды HTTP

КодЗначениеКогда использовать
200OKУспешный GET, PUT, PATCH
201CreatedУспешный POST (ресурс создан)
204No ContentУспешный DELETE (нет тела ответа)
400Bad RequestНеверные параметры запроса
401UnauthorizedТребуется аутентификация
403ForbiddenДоступ запрещён
404Not FoundРесурс не найден
405Method Not AllowedМетод не поддерживается для этого URL
422Unprocessable EntityОшибка валидации данных
500Internal Server ErrorОшибка на сервере

2.4 Структура URL

REST использует ресурсы и операции над ними. URL должны быть понятными и отражать структуру данных.

Примеры:

  • GET /users — список пользователей.
  • GET /users/1 — пользователь с ID 1.
  • GET /users/1/orders — заказы пользователя с ID 1.
  • POST /users — создать пользователя.
  • PUT /users/1 — обновить пользователя.
  • DELETE /users/1 — удалить пользователя.

Правила:

  • Используйте существительные, а не глаголы (/users, а не /getUsers).
  • Используйте множественное число (/users, а не /user).
  • Используйте вложенность для связанных ресурсов (/users/1/orders).
  • Избегайте глубокой вложенности (максимум 2–3 уровня).

2.5 Форматы данных

Стандарт де-факто — JSON. Он лёгкий, читаемый и поддерживается всеми языками.

Пример ответа:

JSON
{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com",
  "created_at": "2024-03-10T12:00:00Z"
}

Пример списка с пагинацией:

JSON
{
  "data": [
    {"id": 1, "name": "John"},
    {"id": 2, "name": "Jane"}
  ],
  "meta": {
    "page": 1,
    "per_page": 10,
    "total": 100
  },
  "links": {
    "self": "/users?page=1",
    "next": "/users?page=2",
    "last": "/users?page=10"
  }
}

Короткий пример реализации пагинации в контроллере:

PHP
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 10;
$offset = ($page - 1) * $perPage;

$stmt = $this->pdo->prepare("SELECT * FROM tasks ORDER BY created_at DESC LIMIT :limit OFFSET :offset");
$stmt->execute([
    ':limit' => $perPage,
    ':offset' => $offset,
]);
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);

3. Создание простого REST API на PHP

Рассмотрим пример API для управления задачами (todo list).

3.1 Структура проекта

3.2 Роутер (index.php)

PHP
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *'); // в продакшене — конкретный домен
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Получаем метод и путь
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = str_replace('/api', '', $path);
$path = trim($path, '/');
$parts = explode('/', $path);

// Подключение к БД
require_once 'config/database.php';
require_once 'controllers/TaskController.php';

$controller = new TaskController($pdo);

// Роутинг
if ($parts[0] === 'tasks') {
    $id = $parts[1] ?? null;
    
    switch ($method) {
        case 'GET':
            if ($id) {
                $controller->getTask($id);
            } else {
                $controller->getTasks();
            }
            break;
        case 'POST':
            $controller->createTask();
            break;
        case 'PUT':
            if ($id) {
                $controller->updateTask($id);
            } else {
                http_response_code(400);
                echo json_encode(['error' => 'ID required']);
            }
            break;
        case 'DELETE':
            if ($id) {
                $controller->deleteTask($id);
            } else {
                http_response_code(400);
                echo json_encode(['error' => 'ID required']);
            }
            break;
        default:
            http_response_code(405);
            echo json_encode(['error' => 'Method not allowed']);
    }
} else {
    http_response_code(404);
    echo json_encode(['error' => 'Not found']);
}

3.3 Подключение к БД (config/database.php)

PHP
<?php
$host = 'localhost';
$dbname = 'api_db';
$user = 'api_user';
$password = 'secure_password';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    http_response_code(500);
    echo json_encode(['error' => 'Database connection failed']);
    exit;
}

3.4 Контроллер (controllers/TaskController.php)

PHP
<?php
class TaskController {
    private $pdo;

    public function __construct($pdo) {
        $this->pdo = $pdo;
    }

    // GET /tasks
    //ВАЖНО! Здесь нет пользовательских данных, поэтому query допустим; во всех остальных случаях — только prepare
    public function getTasks() {
        $stmt = $this->pdo->query("SELECT * FROM tasks ORDER BY created_at DESC");
        $tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
        echo json_encode(['data' => $tasks]);
    }

    // GET /tasks/{id}
    public function getTask($id) {
        $stmt = $this->pdo->prepare("SELECT * FROM tasks WHERE id = ?");
        $stmt->execute([$id]);
        $task = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($task) {
            echo json_encode(['data' => $task]);
        } else {
            http_response_code(404);
            echo json_encode(['error' => 'Task not found']);
        }
    }

    // POST /tasks
    public function createTask() {
        $data = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);

        if (empty($data['title'])) {
        http_response_code(422);
        echo json_encode(['error' => 'Title is required']);
        return;
}

        $allowedStatuses = ['pending', 'in_progress', 'done'];
        $status = $data['status'] ?? 'pending';
        if (!in_array($status, $allowedStatuses, true)) {
        http_response_code(422);
        echo json_encode(['error' => 'Invalid status']);
        return;
}

        $stmt = $this->pdo->prepare("INSERT INTO tasks (title, description, status) VALUES (?, ?, ?)");
        $stmt->execute([$data['title'], $data['description'] ?? '', 'pending']);

        $id = $this->pdo->lastInsertId();
        http_response_code(201);
        echo json_encode(['data' => ['id' => $id, 'title' => $data['title']]]);
    }

    // PUT /tasks/{id}
    public function updateTask($id) {
        $data = json_decode(file_get_contents('php://input'), true);

        $stmt = $this->pdo->prepare("UPDATE tasks SET title = ?, description = ?, status = ? WHERE id = ?");
        $stmt->execute([
            $data['title'] ?? '',
            $data['description'] ?? '',
            $data['status'] ?? 'pending',
            $id
        ]);

        if ($stmt->rowCount() > 0) {
            echo json_encode(['message' => 'Task updated']);
        } else {
            http_response_code(404);
            echo json_encode(['error' => 'Task not found']);
        }
    }

    // DELETE /tasks/{id}
    public function deleteTask($id) {
        $stmt = $this->pdo->prepare("DELETE FROM tasks WHERE id = ?");
        $stmt->execute([$id]);

        if ($stmt->rowCount() > 0) {
            http_response_code(204);
        } else {
            http_response_code(404);
            echo json_encode(['error' => 'Task not found']);
        }
    }
}

3.5 .htaccess для перенаправления

Apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

Теперь API доступно по адресам:

  • GET /api/tasks
  • GET /api/tasks/1
  • POST /api/tasks
  • PUT /api/tasks/1
  • DELETE /api/tasks/1

4. Аутентификация и авторизация

API должны быть защищены. Рассмотрим основные методы.

4.1 Basic Auth

Самый простой метод: логин и пароль передаются в заголовке Authorization в base64.

Плюсы: простота.
Минусы: пароль передаётся в открытом виде (только с HTTPS), нет гибкости.

Пример на PHP:

PHP
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
$pass = $_SERVER['PHP_AUTH_PW'] ?? '';

if ($user !== 'admin' || $pass !== 'secret') {
    http_response_code(401);
    header('WWW-Authenticate: Basic realm="API"');
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

ВАЖНО! Basic Auth требует корректной настройки веб‑сервера; для реальных API лучше использовать API‑ключи или JWT

4.2 API-ключи

Клиент передаёт ключ в заголовке или параметре.

Плюсы: простота, можно отозвать ключ.
Минусы: ключ может утечь, нет срока действия (если не реализовано).

Пример:

PHP
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';

$stmt = $pdo->prepare("SELECT * FROM api_keys WHERE key = ? AND active = 1");
$stmt->execute([$apiKey]);
$key = $stmt->fetch();

if (!$key) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid API key']);
    exit;
}

4.3 JWT (JSON Web Token)

JWT — это токен, который содержит закодированную информацию о пользователе и подписан секретным ключом.

Структура JWT:

  • Header (алгоритм, тип).
  • Payload (данные: user_id, exp, iat).
  • Signature (подпись).

Пример создания JWT на PHP:

PHP
function generateJWT($userId, $secret) {
    $header = base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
    $payload = base64_encode(json_encode([
        'user_id' => $userId,
        'iat' => time(),
        'exp' => time() + 3600
    ]));
    $signature = base64_encode(hash_hmac('sha256', "$header.$payload", $secret, true));
    return "$header.$payload.$signature";
}

function validateJWT($token, $secret) {
    $parts = explode('.', $token);
    if (count($parts) !== 3) return false;

    [$header, $payload, $signature] = $parts;
    $validSignature = base64_encode(hash_hmac('sha256', "$header.$payload", $secret, true));

    if ($signature !== $validSignature) return false;

    $data = json_decode(base64_decode($payload), true);
    if ($data['exp'] < time()) return false;

    return $data;
}

ВАЖНО! В примере это демонстрация принципа; в реальном проекте используйте проверенные библиотеки.

Использование:

PHP
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = str_replace('Bearer ', '', $token);

$user = validateJWT($token, 'your_secret_key');
if (!$user) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid token']);
    exit;
}

// Теперь $user['user_id'] доступен

Плюсы JWT: stateless, можно хранить данные, подпись защищает от подделки.
Минусы: нельзя отозвать до истечения срока, нужно хранить секрет.

Для PHP рекомендуем firebase/php-jwt (стандарт индустрии):

Bash
composer require firebase/php-jwt

Пример безопасного создания токена:

PHP
use Firebase\JWT\JWT;

$payload = [
    'user_id' => 123,
    'iat' => time(),
    'exp' => time() + 3600,
];
$secret = getenv('JWT_SECRET'); // из .env
$token = JWT::encode($payload, $secret, 'HS256');

Валидация:

PHP
try {
    $decoded = JWT::decode($token, $secret, ['HS256']);
} catch (\Exception $e) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid token']);
    exit;
}

4.4 OAuth2

OAuth2 — протокол авторизации, который позволяет пользователям предоставлять доступ к своим данным без передачи пароля. Используется для входа через Google, GitHub, VK и т.д.

Основные понятия:

  • Client ID и Client Secret — идентификаторы приложения.
  • Authorization Code — временный код для получения токена.
  • Access Token — токен для доступа к API.
  • Refresh Token — токен для обновления access token.

Схема:

  1. Пользователь перенаправляется на сервер авторизации.
  2. Пользователь подтверждает доступ.
  3. Сервер возвращает authorization code.
  4. Приложение обменивает код на access token.
  5. Приложение использует access token для запросов к API.

Реализация OAuth2 — тема отдельной статьи, но большинство фреймворков имеют готовые библиотеки.

5. GraphQL

GraphQL — это язык запросов для API, разработанный Facebook в 2015 году. В отличие от REST, где клиент получает фиксированную структуру ответа, GraphQL позволяет клиенту точно указать, какие поля ему нужны.

5.1 Основные понятия

  • Schema — описание типов данных и доступных запросов.
  • Query — запрос на чтение данных.
  • Mutation — запрос на изменение данных.
  • Subscription — подписка на обновления в реальном времени.
  • Resolver — функция, которая возвращает данные для поля.

5.2 Пример схемы

GraphQL
type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post]
}

type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
}

type Query {
    users: [User]
    user(id: ID!): User
    posts: [Post]
}

type Mutation {
    createUser(name: String!, email: String!): User
    updateUser(id: ID!, name: String): User
    deleteUser(id: ID!): Boolean
}

5.3 Пример запроса

Клиент запрашивает только нужные поля:

GraphQL
query {
    user(id: 1) {
        name
        email
        posts {
            title
        }
    }
}

Ответ:

JSON
{
    "data": {
        "user": {
            "name": "John Doe",
            "email": "john@example.com",
            "posts": [
                {"title": "First post"},
                {"title": "Second post"}
            ]
        }
    }
}

5.4 Пример мутации

GraphQL
mutation {
    createUser(name: "Jane", email: "jane@example.com") {
        id
        name
    }
}

5.5 Реализация GraphQL на PHP

Используйте библиотеку webonyx/graphql-php (устанавливается через Composer).

Пример:

PHP
<?php
require_once 'vendor/autoload.php';

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\GraphQL;
use GraphQL\Type\Schema;

$userType = new ObjectType([
    'name' => 'User',
    'fields' => [
        'id' => Type::int(),
        'name' => Type::string(),
        'email' => Type::string(),
    ]
]);

$queryType = new ObjectType([
    'name' => 'Query',
    'fields' => [
        'user' => [
            'type' => $userType,
            'args' => ['id' => Type::int()],
            'resolve' => function ($root, $args) use ($pdo) {
                $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
                $stmt->execute([$args['id']]);
                return $stmt->fetch(PDO::FETCH_ASSOC);
            }
        ],
        'users' => [
            'type' => Type::listOf($userType),
            'resolve' => function () use ($pdo) {
                $stmt = $pdo->query("SELECT * FROM users");
                return $stmt->fetchAll(PDO::FETCH_ASSOC);
            }
        ]
    ]
]);

$schema = new Schema(['query' => $queryType]);

$input = json_decode(file_get_contents('php://input'), true);
$result = GraphQL::executeQuery($schema, $input['query']);
echo json_encode($result->toArray());

6. Сравнение REST и GraphQL

КритерийRESTGraphQL
Конечные точкиМного (по одной на ресурс)Одна (/graphql)
Получение данныхФиксированная структураКлиент указывает, что нужно
Избыточность данныхМожет быть (over-fetching)Нет
Недостаток данныхМожет быть (under-fetching)Нет (можно запросить всё за раз)
КешированиеЛегко (HTTP-кеш)Сложнее (нужны свои механизмы)
СложностьПроще для простых APIСложнее в настройке
ВерсионированиеЧерез URL (/v1/users)Через эволюцию схемы
ФайлыЛегко (multipart)Требует отдельных решений
ИнструментыШирокий выборGraphQL Playground, Apollo

Когда выбирать REST:

  • Простой API с предсказуемыми запросами.
  • Нужно кеширование на уровне HTTP.
  • Команда незнакома с GraphQL.
  • Файлы и бинарные данные.

Когда выбирать GraphQL:

  • Сложные связи между данными.
  • Мобильные приложения с ограниченным трафиком.
  • Быстро меняющиеся требования к данным.
  • Микросервисная архитектура с единой точкой входа.

7. Документирование API

Документация — это лицо вашего API. Без неё разработчики не смогут им пользоваться.

7.1 Swagger / OpenAPI

OpenAPI — стандарт описания REST API. Swagger — инструмент для визуализации.

Пример спецификации:

YAML
openapi: 3.0.0
info:
  title: Task API
  version: 1.0.0
paths:
  /tasks:
    get:
      summary: Список задач
      responses:
        '200':
          description: Успешный ответ
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Task'
    post:
      summary: Создать задачу
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskInput'
      responses:
        '201':
          description: Задача создана
components:
  schemas:
    Task:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        status:
          type: string
    TaskInput:
      type: object
      required:
        - title
      properties:
        title:
          type: string
        description:
          type: string

Инструменты:

  • Swagger UI — интерактивная документация.
  • ReDoc — красивое отображение.
  • Postman — тестирование и документация.

7.2 GraphQL Playground

GraphQL имеет встроенный инструмент — GraphQL Playground (или GraphiQL). Он позволяет исследовать схему и выполнять запросы.

8. Версионирование API

Со временем API меняется. Чтобы не ломать существующих клиентов, используйте версионирование.

Способы:

  • URL: /v1/users/v2/users.
  • Заголовок: Accept: application/vnd.api.v1+json.
  • Параметр: /users?version=1.

Рекомендация: используйте URL-версионирование — оно самое простое и понятное.

9. Практические советы и частые ошибки

9.1 Советы

  • Всегда используйте HTTPS. API без шифрования — это утечка данных.
  • Валидируйте входные данные. Никогда не доверяйте клиенту.
  • Ограничивайте частоту запросов (rate limiting). Защита от DDoS и брутфорса.
  • Логируйте запросы. Это поможет отладить проблемы.
  • Возвращайте понятные ошибки. С кодом, сообщением и, возможно, ссылкой на документацию.
  • Используйте пагинацию. Не отдавайте все данные сразу.
  • Документируйте. Без документации API бесполезен.

Пример простого rate limiting на уровне PHP:

PHP
// Псевдокод: проверить количество запросов за минуту по IP
if ($requestsPerMinute > 60) {
    http_response_code(429);
    echo json_encode(['error' => 'Too many requests']);
    exit;
}

9.2 Частые ошибки

ОшибкаПоследствияРешение
Отсутствие HTTPSДанные перехватываютсяВсегда используйте SSL/TLS.
Секреты в кодеУтечкаИспользуйте переменные окружения.
Нет валидацииSQL-инъекции, XSSВалидируйте и фильтруйте все входные данные.
Возврат 200 при ошибкеКлиент не понимает, что что-то не такИспользуйте правильные статус-коды.
Нет пагинацииСервер падает под нагрузкойВсегда ограничивайте выборку.
Игнорирование rate limitingDDoSНастройте ограничение запросов.
Отсутствие версионированияЛомает клиентов при обновленииВерсионируйте API с самого начала.
Нет документацииРазработчики не могут использоватьИспользуйте OpenAPI/Swagger.

Мы научились создавать REST API на PHP, защищать его с помощью JWT и API-ключей, документировать через OpenAPI и версионировать. Эти знания помогут вам строить надёжные и удобные интерфейсы для любых приложений — от мобильных клиентов до микросервисов.

Нашли ошибку? Напишите нам!