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
- Клиент-сервер — клиент и сервер разделены, каждый может развиваться независимо.
- Отсутствие состояния (Stateless) — каждый запрос содержит всю информацию, необходимую для его обработки. Сервер не хранит состояние клиента между запросами.
- Кеширование — ответы могут кешироваться.
- Единообразие интерфейса — использование стандартных HTTP-методов и URL.
- Слои — клиент может не знать, общается ли он напрямую с сервером или через прокси.
- Код по требованию (опционально) — сервер может передавать клиенту исполняемый код.
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
| Код | Значение | Когда использовать |
|---|---|---|
| 200 | OK | Успешный GET, PUT, PATCH |
| 201 | Created | Успешный POST (ресурс создан) |
| 204 | No Content | Успешный DELETE (нет тела ответа) |
| 400 | Bad Request | Неверные параметры запроса |
| 401 | Unauthorized | Требуется аутентификация |
| 403 | Forbidden | Доступ запрещён |
| 404 | Not Found | Ресурс не найден |
| 405 | Method Not Allowed | Метод не поддерживается для этого URL |
| 422 | Unprocessable Entity | Ошибка валидации данных |
| 500 | Internal 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. Он лёгкий, читаемый и поддерживается всеми языками.
Пример ответа:
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-03-10T12:00:00Z"
}Пример списка с пагинацией:
{
"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"
}
}Короткий пример реализации пагинации в контроллере:
$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 Структура проекта
api/
├── index.php # Точка входа (роутер)
├── config/
│ └── database.php # Подключение к БД
├── controllers/
│ └── TaskController.php
├── models/
│ └── Task.php
└── .htaccess # Перенаправление всех запросов на index.php
3.2 Роутер (index.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
$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
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 для перенаправления
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]Теперь API доступно по адресам:
GET /api/tasksGET /api/tasks/1POST /api/tasksPUT /api/tasks/1DELETE /api/tasks/1
4. Аутентификация и авторизация
API должны быть защищены. Рассмотрим основные методы.
4.1 Basic Auth
Самый простой метод: логин и пароль передаются в заголовке Authorization в base64.
Authorization: Basic dXNlcjpwYXNzd29yZA==
Плюсы: простота.
Минусы: пароль передаётся в открытом виде (только с HTTPS), нет гибкости.
Пример на 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-ключи
Клиент передаёт ключ в заголовке или параметре.
X-API-Key: your_api_key_here
Плюсы: простота, можно отозвать ключ.
Минусы: ключ может утечь, нет срока действия (если не реализовано).
Пример:
$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:
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;
}ВАЖНО! В примере это демонстрация принципа; в реальном проекте используйте проверенные библиотеки.
Использование:
$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 (стандарт индустрии):
composer require firebase/php-jwtПример безопасного создания токена:
use Firebase\JWT\JWT;
$payload = [
'user_id' => 123,
'iat' => time(),
'exp' => time() + 3600,
];
$secret = getenv('JWT_SECRET'); // из .env
$token = JWT::encode($payload, $secret, 'HS256');Валидация:
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.
Схема:
- Пользователь перенаправляется на сервер авторизации.
- Пользователь подтверждает доступ.
- Сервер возвращает authorization code.
- Приложение обменивает код на access token.
- Приложение использует access token для запросов к API.
Реализация OAuth2 — тема отдельной статьи, но большинство фреймворков имеют готовые библиотеки.
5. GraphQL
GraphQL — это язык запросов для API, разработанный Facebook в 2015 году. В отличие от REST, где клиент получает фиксированную структуру ответа, GraphQL позволяет клиенту точно указать, какие поля ему нужны.
5.1 Основные понятия
- Schema — описание типов данных и доступных запросов.
- Query — запрос на чтение данных.
- Mutation — запрос на изменение данных.
- Subscription — подписка на обновления в реальном времени.
- Resolver — функция, которая возвращает данные для поля.
5.2 Пример схемы
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 Пример запроса
Клиент запрашивает только нужные поля:
query {
user(id: 1) {
name
email
posts {
title
}
}
}Ответ:
{
"data": {
"user": {
"name": "John Doe",
"email": "john@example.com",
"posts": [
{"title": "First post"},
{"title": "Second post"}
]
}
}
}5.4 Пример мутации
mutation {
createUser(name: "Jane", email: "jane@example.com") {
id
name
}
}5.5 Реализация GraphQL на PHP
Используйте библиотеку webonyx/graphql-php (устанавливается через Composer).
Пример:
<?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
| Критерий | REST | GraphQL |
|---|---|---|
| Конечные точки | Много (по одной на ресурс) | Одна (/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 — инструмент для визуализации.
Пример спецификации:
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:
// Псевдокод: проверить количество запросов за минуту по 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 limiting | DDoS | Настройте ограничение запросов. |
| Отсутствие версионирования | Ломает клиентов при обновлении | Версионируйте API с самого начала. |
| Нет документации | Разработчики не могут использовать | Используйте OpenAPI/Swagger. |
Мы научились создавать REST API на PHP, защищать его с помощью JWT и API-ключей, документировать через OpenAPI и версионировать. Эти знания помогут вам строить надёжные и удобные интерфейсы для любых приложений — от мобильных клиентов до микросервисов.
