Проектирование REST API

Проектирование REST API

REST (Representational State Transfer) - это архитектурный стиль для построения распределённых систем поверх HTTP. В этом уроке мы изучим, как создавать понятные, масштабируемые и удобные API.

Принципы REST

1. Client-Server

Клиент и сервер разделены. Клиент не знает о хранении данных, сервер не знает об UI.

2. Stateless

Каждый запрос содержит всю информацию для его обработки. Сервер не хранит состояние клиента между запросами.

# Плохо - сервер помнит контекст
POST /api/search
{ "query": "phones" }

GET /api/next-page  # Сервер должен помнить предыдущий поиск

# Хорошо - каждый запрос самодостаточен
GET /api/products?query=phones&page=1
GET /api/products?query=phones&page=2

3. Cacheable

Ответы должны указывать, можно ли их кешировать.

HTTP/1.1 200 OK
Cache-Control: max-age=3600, public
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

4. Uniform Interface

Единообразный интерфейс для всех ресурсов.

5. Layered System

Архитектура может состоять из иерархических слоёв.

6. Code on Demand (опционально)

Сервер может отправлять исполняемый код клиенту.

Ресурсы и URL

REST: ресурс как существительное, метод HTTP как действие

Используйте существительные, не глаголы

# Плохо
GET /getUsers
POST /createUser
POST /deleteUser/123

# Хорошо
GET /users
POST /users
DELETE /users/123

Используйте множественное число

# Плохо
GET /user
GET /user/123

# Хорошо
GET /users
GET /users/123

Иерархия ресурсов

# Компании
GET /companies
GET /companies/123

# Сотрудники компании
GET /companies/123/employees
GET /companies/123/employees/456

# Можно также обратиться напрямую
GET /employees/456

HTTP методы и их семантика

GET - получение ресурса

GET /users/123 HTTP/1.1

HTTP/1.1 200 OK
{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com"
}

POST - создание ресурса

POST /users HTTP/1.1
Content-Type: application/json

{
  "name": "Jane Doe",
  "email": "jane@example.com"
}

HTTP/1.1 201 Created
Location: /users/124
{
  "id": 124,
  "name": "Jane Doe",
  "email": "jane@example.com"
}

PUT - полное обновление

PUT /users/123 HTTP/1.1
Content-Type: application/json

{
  "name": "John Smith",
  "email": "john.smith@example.com"
}

HTTP/1.1 200 OK
{
  "id": 123,
  "name": "John Smith",
  "email": "john.smith@example.com"
}

PATCH - частичное обновление

PATCH /users/123 HTTP/1.1
Content-Type: application/json

{
  "email": "newemail@example.com"
}

HTTP/1.1 200 OK
{
  "id": 123,
  "name": "John Smith",
  "email": "newemail@example.com"
}

DELETE - удаление

DELETE /users/123 HTTP/1.1

HTTP/1.1 204 No Content

Фильтрация, сортировка и пагинация

Фильтрация

# Простая фильтрация
GET /products?category=electronics&price_min=100&price_max=500

# Сложная фильтрация
GET /products?filter[category]=electronics&filter[price][gte]=100&filter[price][lte]=500

Сортировка

# Простая сортировка
GET /products?sort=price
GET /products?sort=-price  # По убыванию

# Множественная сортировка
GET /products?sort=-price,name

Пагинация

Offset-based:

GET /products?offset=20&limit=10

{
  "data": [...],
  "meta": {
    "total": 100,
    "offset": 20,
    "limit": 10
  }
}

Page-based:

GET /products?page=3&per_page=10

{
  "data": [...],
  "meta": {
    "total": 100,
    "page": 3,
    "per_page": 10,
    "total_pages": 10
  }
}

Cursor-based:

GET /products?cursor=eyJpZCI6MTIzfQ==&limit=10

{
  "data": [...],
  "meta": {
    "next_cursor": "eyJpZCI6MTMzfQ==",
    "has_more": true
  }
}

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

1. В URL

GET https://api.example.com/v1/users
GET https://api.example.com/v2/users

2. В заголовке

GET https://api.example.com/users
Accept: application/vnd.myapi.v2+json

3. В query параметре

GET https://api.example.com/users?version=2

Стратегия версионирования

// Поддержка нескольких версий
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);

// Deprecation заголовки
app.use('/api/v1', (req, res, next) => {
  res.set('Sunset', '2024-12-31');
  res.set('Deprecation', 'true');
  res.set('Link', '</api/v2>; rel="successor-version"');
  next();
});

Обработка ошибок

Используйте правильные HTTP коды

// 400 Bad Request - неверный формат запроса
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "Invalid request format",
    "details": {
      "email": "Invalid email format"
    }
  }
}

// 401 Unauthorized - нет аутентификации
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required"
  }
}

// 403 Forbidden - нет прав
{
  "error": {
    "code": "FORBIDDEN",
    "message": "You don't have permission to access this resource"
  }
}

// 404 Not Found - ресурс не найден
{
  "error": {
    "code": "NOT_FOUND",
    "message": "User not found"
  }
}

// 422 Unprocessable Entity - ошибка валидации
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "errors": [
      {
        "field": "email",
        "code": "invalid_format",
        "message": "Email must be a valid email address"
      }
    ]
  }
}

Консистентный формат ошибок

class APIError {
  constructor(code, message, status, details = null) {
    this.error = {
      code,
      message,
      ...(details && { details })
    };
    this.status = status;
  }
}

// Использование
throw new APIError(
  'VALIDATION_ERROR',
  'Validation failed',
  422,
  { email: 'Invalid format' }
);

HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) - клиент взаимодействует с приложением через гиперссылки.

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "_links": {
    "self": {
      "href": "/users/123"
    },
    "posts": {
      "href": "/users/123/posts"
    },
    "following": {
      "href": "/users/123/following"
    },
    "followers": {
      "href": "/users/123/followers"
    }
  }
}

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

OpenAPI (Swagger)

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
  description: API для управления пользователями

paths:
  /users:
    get:
      summary: Получить список пользователей
      parameters:
 - name: page
          in: query
          schema:
            type: integer
            default: 1
 - name: limit
          in: query
          schema:
            type: integer
            default: 10
      responses:
        '200':
          description: Успешный ответ
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  meta:
                    $ref: '#/components/schemas/Pagination'

    post:
      summary: Создать пользователя
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUser'
      responses:
        '201':
          description: Пользователь создан
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email

    CreateUser:
      type: object
      required:
 - name
 - email
      properties:
        name:
          type: string
        email:
          type: string
          format: email

    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer

Инструменты для документации

  1. Swagger UI - интерактивная документация
  2. Postman - коллекции и документация
  3. Redoc - красивая документация из OpenAPI
  4. API Blueprint - markdown-based документация

Best Practices

1. Используйте правильные Content-Type

# JSON
Content-Type: application/json

# XML
Content-Type: application/xml

# Form data
Content-Type: application/x-www-form-urlencoded

# Multipart (файлы)
Content-Type: multipart/form-data

2. Поддерживайте content negotiation

# Клиент запрашивает JSON
GET /users/123
Accept: application/json

# Клиент запрашивает XML
GET /users/123
Accept: application/xml

3. Используйте правильные статус коды

// Создание ресурса
res.status(201).json(user);

// Успешное удаление
res.status(204).send();

// Ресурс не найден
res.status(404).json({ error: 'Not found' });

// Ошибка сервера
res.status(500).json({ error: 'Internal server error' });

4. Идемпотентность

  • GET, PUT, DELETE должны быть идемпотентными
  • POST не идемпотентен
// PUT идемпотентен - многократные вызовы дают тот же результат
PUT /users/123
{ "name": "John", "email": "john@example.com" }

// POST не идемпотентен - каждый вызов создаёт новый ресурс
POST /users
{ "name": "John", "email": "john@example.com" }

5. Безопасность

// Rate limiting
app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
}));

// CORS
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS.split(','),
  credentials: true
}));

// Security headers
app.use(helmet());

// Input validation
app.post('/users',
  body('email').isEmail(),
  body('name').isLength({ min: 2 }),
  handleValidation
);

Итоги

Хорошо спроектированный REST API:

  • Интуитивно понятен
  • Следует стандартам HTTP
  • Имеет предсказуемое поведение
  • Хорошо документирован
  • Безопасен и производителен

Проектирование API - это искусство балансирования между простотой, функциональностью и производительностью. Следуйте принципам REST, но не бойтесь отступать от них, когда это оправдано бизнес-требованиями.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.