REST API: принципы, проектирование, лучшие практики
REST API для фронтенд-разработчика: принципы REST, проектирование эндпоинтов, CRUD-операции, пагинация, фильтрация, версионирование и работа с API.
Что такое REST API
REST (Representational State Transfer) — это стиль архитектуры для создания веб-API. RESTful API — это API, которое следует принципам REST. Важно: REST — это не протокол и не стандарт, а набор рекомендаций.
RESTful API использует HTTP как есть:
- Ресурсы идентифицируются URL (
/users,/posts/1) - Действия — HTTP-методами (
GET,POST,PUT,DELETE) - Данные — в формате JSON
Пример REST API для блога:
GET /posts → Получить все посты
GET /posts/1 → Получить пост с ID 1
POST /posts → Создать новый пост
PUT /posts/1 → Заменить пост с ID 1
PATCH /posts/1 → Обновить пост с ID 1 частично
DELETE /posts/1 → Удалить пост с ID 1
GET /posts/1/comments → Комментарии к посту 1
Принципы REST
1. Ресурсы, а не действия
URL должен обозначать ресурс (существительное), а не действие (глагол). Действие определяется HTTP-методом.
Плохо:
POST /createUser
POST /deleteUser
GET /getUsers
POST /updateUserEmail
Хорошо:
POST /users
DELETE /users/1
GET /users
PATCH /users/1
2. Единообразный интерфейс
Одни и те же HTTP-методы означают одно и то же для всех ресурсов:
GET— чтениеPOST— созданиеPUT/PATCH— обновлениеDELETE— удаление
3. Stateless
Каждый запрос содержит всю информацию для обработки. Сервер не хранит состояние между запросами (сессии — вне REST, но обычно используются).
4. Иерархия ресурсов
Вложенные ресурсы через путь:
/users/1/posts → Посты пользователя 1
/users/1/posts/5 → Пост 5 пользователя 1
/users/1/posts/5/comments → Комментарии к посту 5
Не больше 2–3 уровней вложенности. Если глубже — лучше использовать query-параметры:
/comments?user_id=1&post_id=5
CRUD-операции
CRUD (Create, Read, Update, Delete) — четыре базовых операции.
Create — POST
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
name: 'Анна',
email: 'anna@example.com',
role: 'developer',
}),
})
const user = await response.json()
// { id: 42, name: 'Анна', email: 'anna@example.com', role: 'developer', created_at: '...' }
Статус-код: 201 Created
Ответ: созданный ресурс с id
Read — GET
const response = await fetch('/api/users/42', {
headers: { 'Authorization': `Bearer ${token}` },
})
const user = await response.json()
// { id: 42, name: 'Анна', email: 'anna@example.com' }
Статус-код: 200 OK
Статус-код (не найден): 404 Not Found
Update — PUT / PATCH
PUT (полная замена):
const response = await fetch('/api/users/42', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Анна Иванова',
email: 'anna@new.com',
role: 'senior',
}),
})
PATCH (частичное обновление):
const response = await fetch('/api/users/42', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: 'senior' }),
})
Статус-код: 200 OK (обновлённый ресурс) или 204 No Content (без тела)
Delete — DELETE
await fetch('/api/users/42', { method: 'DELETE' })
Статус-код: 204 No Content или 200 OK (с подтверждением)
Пагинация
Когда ресурсов тысячи, нельзя отдавать всё одним запросом.
Offset-пагинация
GET /api/users?page=2&limit=20
Ответ:
{
"data": [
{ "id": 21, "name": "..." },
{ "id": 22, "name": "..." }
],
"pagination": {
"page": 2,
"limit": 20,
"total": 156,
"total_pages": 8
}
}
Проблема: при добавлении новых записей между запросами offset может сдвинуться (дубли или пропуски).
Cursor-пагинация
GET /api/users?cursor=eyJpZCI6MjB9&limit=20
Ответ:
{
"data": [...],
"next_cursor": "eyJpZCI6NDB9",
"has_more": true
}
Cursor = идентификатор последнего элемента. Нет проблем с дублированием. Используется в Twitter, Facebook, Slack API.
Фильтрация и сортировка
Фильтрация через query-параметры
GET /api/users?role=developer&status=active
GET /api/posts?created_after=2025-01-01&author_id=5
GET /api/products?price_min=100&price_max=500&category=electronics
Сортировка
GET /api/users?sort=created_at&order=desc
GET /api/posts?sort=-created_at,title
Конвенция с - для desc: sort=-created_at = по дате убывания.
Поиск
GET /api/users?search=anna
GET /api/posts?q=vue+typescript
Поля (sparse fieldsets)
GET /api/users?fields=id,name,email
Возвращает только указанные поля — экономит трафик.
Версионирование API
API меняется со временем. Версионирование позволяет не ломать существующих клиентов.
URL-версионирование (самое простое)
GET /api/v1/users
GET /api/v2/users
Header-версионирование
GET /api/users
Accept: application/vnd.myapi.v2+json
Query-параметр
GET /api/users?version=2
Рекомендация: URL-версионирование (/v1/, /v2/) — самое понятное.
Формат ответа
Успешный ответ ( единообразная обёртка)
{
"data": { ... },
"message": "User created successfully"
}
Для списков:
{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 156
}
}
Ошибка
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{ "field": "email", "message": "Email is required" },
{ "field": "name", "message": "Name must be at least 2 characters" }
]
}
}
Авторизация
Bearer Token (JWT)
const token = localStorage.getItem('token')
const response = await fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`,
},
})
API Key
const response = await fetch('/api/data', {
headers: {
'X-API-Key': 'abc123def456',
},
})
Обработка ошибок на фронтенде
class ApiError extends Error {
constructor(
public status: number,
public code: string,
message: string,
public details?: Array<{ field: string; message: string }>,
) {
super(message)
}
}
async function api<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(`/api${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
...options?.headers,
},
})
if (!response.ok) {
const body = await response.json()
throw new ApiError(
response.status,
body.error?.code ?? 'UNKNOWN',
body.error?.message ?? 'Unknown error',
body.error?.details,
)
}
return response.json()
}
// Использование
try {
const users = await api<User[]>('/users')
const user = await api<User>('/users/1')
const newUser = await api<User>('/users', {
method: 'POST',
body: JSON.stringify({ name: 'Анна' }),
})
} catch (error) {
if (error instanceof ApiError) {
if (error.status === 422) {
error.details?.forEach((d) => {
console.error(`${d.field}: ${d.message}`)
})
}
}
}
Пример: полноценный API-клиент
class ApiClient {
private baseUrl: string
constructor(baseUrl: string) {
this.baseUrl = baseUrl
}
private async request<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${this.baseUrl}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...this.authHeaders(),
...options?.headers,
},
})
if (response.status === 204) return undefined as T
if (!response.ok) {
const error = await response.json()
throw new ApiError(response.status, error.code, error.message)
}
return response.json()
}
private authHeaders() {
const token = localStorage.getItem('token')
return token ? { Authorization: `Bearer ${token}` } : {}
}
get<T>(url: string, params?: Record<string, string>) {
const query = params ? '?' + new URLSearchParams(params).toString() : ''
return this.request<T>(url + query)
}
post<T>(url: string, body: unknown) {
return this.request<T>(url, { method: 'POST', body: JSON.stringify(body) })
}
put<T>(url: string, body: unknown) {
return this.request<T>(url, { method: 'PUT', body: JSON.stringify(body) })
}
patch<T>(url: string, body: unknown) {
return this.request<T>(url, { method: 'PATCH', body: JSON.stringify(body) })
}
delete(url: string) {
return this.request<void>(url, { method: 'DELETE' })
}
}
const api = new ApiClient('/api')
// Использование
const users = await api.get<User[]>('/users', { page: '1', limit: '20' })
const user = await api.post<User>('/users', { name: 'Анна', email: 'anna@test.com' })
await api.patch(`/users/${user.id}`, { name: 'Анна Иванова' })
await api.delete(`/users/${user.id}`)
Best Practices
URL
- Существительные во множественном числе:
/users,/posts - Строчные буквы, дефисы:
/user-profiles, не/userProfiles - Вложенность до 2 уровней:
/users/1/posts - ID в пути:
/users/42
Ответы
200для GET, PUT, PATCH201для POST (создание)204для DELETE422для ошибок валидации- Единообразная структура JSON-ответа
Безопасность
- HTTPS обязательно
- Авторизация через Bearer Token
- Rate limiting для защиты от DDoS
- Валидация на сервере (не только на клиенте)
Документация
- Swagger/OpenAPI — стандарт описания REST API
- Примеры запросов и ответов
- Описание всех статус-кодов
Итог
- REST API использует HTTP-методы для CRUD-операций над ресурсами
- URL = ресурс (существительное), метод = действие
- GET — читать, POST — создавать, PUT/PATCH — обновлять, DELETE — удалять
- Пагинация, фильтрация и сортировка через query-параметры
- Версионирование через
/v1/,/v2/в URL - Создайте API-клиент с единообразной обработкой ошибок
- Документируйте API через Swagger/OpenAPI