Skip to content
IRC-CodingIRC-Coding
REST APIHTTP методыстатус-кодыHATEOASRichardson Maturity

Основы REST API: HTTP-методы и статус-коды

REST API с HTTP-методами, статус-кодами, HATEOAS и Richardson Maturity Model. Примеры GET, POST, PUT, DELETE.

S

schutzgeist

7 min read
Основы REST API: HTTP-методы и статус-коды

Основы REST API: HTTP-методы, коды состояния и HATEOAS

Этот материал представляет собой полное объяснение основ REST API, включая HTTP-методы, коды состояния и принципы HATEOAS.

Суть в двух словах

REST — это архитектурный стиль для распределённых систем. Он использует HTTP-методы, коды состояния и HATEOAS для создания масштабируемых и stateless веб-сервисов.

Определение

Representational State Transfer (REST) — архитектурный стиль для веб-сервисов, определённый Roy Fielding. REST опирается на семантику HTTP при работе с ресурсами.

Основные принципы:

  • Client-Server: разделение ответственности между клиентом и сервером
  • Stateless: сервер не хранит состояние сессии
  • Cacheable: ответы можно кэшировать
  • Uniform Interface: единая интерфейс через HTTP
  • Layered System: возможны промежуточные слои
  • Code on Demand: опционально, сервер может отправить код клиенту

HTTP-методы:

  • GET: чтение ресурса (безопасен, идемпотентен)
  • POST: создание ресурса (небезопасен, не идемпотентен)
  • PUT: замена ресурса (небезопасен, идемпотентен)
  • PATCH: частичное изменение ресурса (небезопасен, не идемпотентен)
  • DELETE: удаление ресурса (небезопасен, идемпотентен)

HATEOAS (Hypermedia as the Engine of Application State) позволяет навигировать по API без жёстко закодированных URL.

Ключевые моменты для проверки знаний

  • HTTP-методы: GET, POST, PUT, DELETE с правильной семантикой
  • Коды состояния: 2xx (успех), 3xx (перенаправление), 4xx (ошибка клиента), 5xx (ошибка сервера)
  • HATEOAS: гипермедиа как управление состоянием приложения
  • Stateless: отсутствие состояния сессии на стороне сервера
  • Richardson Maturity Model: модель зрелости для REST API
  • Именование ресурсов: консистентная структура и номенклатура URI
  • Значимо для веб-разработки и архитектуры ПО

Основные компоненты

  1. Ресурсы: уникальные идентификаторы (URI) для данных
  2. HTTP-методы: CRUD-операции через HTTP-глаголы
  3. Коды состояния: стандартизированные коды ответа
  4. Представления: JSON, XML, HTML как форматы данных
  5. Гипермедиа: ссылки для навигации между ресурсами
  6. Отсутствие состояния: каждый запрос содержит всю необходимую информацию
  7. Кэшируемость: ответы можно сохранять
  8. Многоуровневая система: балансировщики нагрузки, прокси, шлюзы

Примеры на практике

1. Дизайн ресурсов и HTTP-методы

// Пример REST API на Express.js
const express = require('express');
const app = express();
app.use(express.json());

// Данные (в памяти)
let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];

// GET /users - получить всех пользователей
app.get('/users', (req, res) => {
  res.status(200).json({
    users: users,
    _links: {
      self: { href: '/users' },
      create: { href: '/users', method: 'POST' }
    }
  });
});

// GET /users/{id} - получить одного пользователя
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  
  if (!user) {
    return res.status(404).json({
      error: 'User not found',
      _links: {
        users: { href: '/users' }
      }
    });
  }
  
  res.status(200).json({
    user: user,
    _links: {
      self: { href: `/users/${user.id}` },
      update: { href: `/users/${user.id}`, method: 'PUT' },
      delete: { href: `/users/${user.id}`, method: 'DELETE' },
      users: { href: '/users' }
    }
  });
});

// POST /users - создать нового пользователя
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({
      error: 'Name and email are required',
      _links: {
        users: { href: '/users' }
      }
    });
  }
  
  const newUser = {
    id: users.length + 1,
    name,
    email
  };
  
  users.push(newUser);
  
  res.status(201).json({
    message: 'User created successfully',
    user: newUser,
    _links: {
      self: { href: `/users/${newUser.id}` },
      users: { href: '/users' }
    }
  });
});

// PUT /users/{id} - полностью заменить пользователя
app.put('/users/:id', (req, res) => {
  const { name, email } = req.body;
  const userId = parseInt(req.params.id);
  
  const userIndex = users.findIndex(u => u.id === userId);
  
  if (userIndex === -1) {
    return res.status(404).json({
      error: 'User not found',
      _links: {
        users: { href: '/users' }
      }
    });
  }
  
  if (!name || !email) {
    return res.status(400).json({
      error: 'Name and email are required',
      _links: {
        user: { href: `/users/${userId}` }
      }
    });
  }
  
  users[userIndex] = { id: userId, name, email };
  
  res.status(200).json({
    message: 'User updated successfully',
    user: users[userIndex],
    _links: {
      self: { href: `/users/${userId}` },
      users: { href: '/users' }
    }
  });
});

// DELETE /users/{id} - удалить пользователя
app.delete('/users/:id', (req, res) => {
  const userId = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === userId);
  
  if (userIndex === -1) {
    return res.status(404).json({
      error: 'User not found',
      _links: {
        users: { href: '/users' }
      }
    });
  }
  
  users.splice(userIndex, 1);
  
  res.status(200).json({
    message: 'User deleted successfully',
    _links: {
      users: { href: '/users' },
      create: { href: '/users', method: 'POST' }
    }
  });
});

app.listen(3000, () => {
  console.log('REST API Server running on port 3000');
});

2. REST API на Spring Boot с HATEOAS

@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    // GET /api/products - получить все продукты с HATEOAS
    @GetMapping
    public ResponseEntity<CollectionModel<EntityModel<Product>>> getAllProducts() {
        List<Product> products = productService.findAll();
        
        List<EntityModel<Product>> productModels = products.stream()
            .map(product -> EntityModel.of(product,
                linkTo(methodOn(ProductController.class).getProduct(product.getId())).withSelfRel(),
                linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products")
            ))
            .collect(Collectors.toList());
        
        CollectionModel<EntityModel<Product>> collectionModel = 
            CollectionModel.of(productModels,
                linkTo(methodOn(ProductController.class).getAllProducts()).withSelfRel()
            );
        
        return ResponseEntity.ok(collectionModel);
    }
    
    // GET /api/products/{id} - получить один продукт с HATEOAS
    @GetMapping("/{id}")
    public ResponseEntity<EntityModel<Product>> getProduct(@PathVariable Long id) {
        return productService.findById(id)
            .map(product -> EntityModel.of(product,
                linkTo(methodOn(ProductController.class).getProduct(id)).withSelfRel(),
                linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products"),
                linkTo(methodOn(ProductController.class).updateProduct(id, null)).withRel("update"),
                linkTo(methodOn(ProductController.class).deleteProduct(id)).withRel("delete")
            ))
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    // POST /api/products - создать продукт
    @PostMapping
    public ResponseEntity<EntityModel<Product>> createProduct(@RequestBody Product product) {
        Product createdProduct = productService.save(product);
        
        EntityModel<Product> productModel = EntityModel.of(createdProduct,
            linkTo(methodOn(ProductController.class).getProduct(createdProduct.getId())).withSelfRel(),
            linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products")
        );
        
        return ResponseEntity
            .created(URI.create("/api/products/" + createdProduct.getId()))
            .body(productModel);
    }
    
    // PUT /api/products/{id} - обновить продукт
    @PutMapping("/{id}")
    public ResponseEntity<EntityModel<Product>> updateProduct(
            @PathVariable Long id, @RequestBody Product product) {
        
        return productService.findById(id)
            .map(existingProduct -> {
                product.setId(id);
                Product updatedProduct = productService.save(product);
                
                EntityModel<Product> productModel = EntityModel.of(updatedProduct,
                    linkTo(methodOn(ProductController.class).getProduct(id)).withSelfRel(),
                    linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products")
                );
                
                return ResponseEntity.ok(productModel);
            })
            .orElse(ResponseEntity.notFound().build());
    }
    
    // DELETE /api/products/{id} - удалить продукт
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        if (productService.existsById(id)) {
            productService.deleteById(id);
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

3. Python Flask REST API

from flask import Flask, jsonify, request, url_for
from werkzeug.exceptions import NotFound, BadRequest

app = Flask(__name__)

# In-memory Datenbank
products = [
    {'id': 1, 'name': 'Laptop', 'price': 999.99, 'category': 'Electronics'},
    {'id': 2, 'name': 'Mouse', 'price': 29.99, 'category': 'Electronics'}
]

def generate_links(product_id=None):
    """HATEOAS Links generieren"""
    links = {
        'products': {'href': url_for('get_products', _external=True)}
    }
    
    if product_id:
        links.update({
            'self': {'href': url_for('get_product', id=product_id, _external=True)},
            'update': {'href': url_for('update_product', id=product_id, _external=True)},
            'delete': {'href': url_for('delete_product', id=product_id, _external=True)}
        })
    
    return links

@app.route('/api/products', methods=['GET'])
def get_products():
    """Alle Produkte abrufen"""
    return jsonify({
        'products': products,
        '_links': generate_links()
    }), 200

@app.route('/api/products/<int:product_id>', methods=['GET'])
def get_product(product_id):
    """Einzelnes Produkt abrufen"""
    product = next((p for p in products if p['id'] == product_id), None)
    
    if not product:
        return jsonify({
            'error': 'Product not found',
            '_links': generate_links()
        }), 404
    
    return jsonify({
        'product': product,
        '_links': generate_links(product_id)
    }), 200

@app.route('/api/products', methods=['POST'])
def create_product():
    """Neues Produkt erstellen"""
    data = request.get_json()
    
    if not data or 'name' not in data or 'price' not in data:
        return jsonify({
            'error': 'Name and price are required',
            '_links': generate_links()
        }), 400
    
    new_product = {
        'id': len(products) + 1,
        'name': data['name'],
        'price': data['price'],
        'category': data.get('category', 'Uncategorized')
    }
    
    products.append(new_product)
    
    return jsonify({
        'message': 'Product created successfully',
        'product': new_product,
        '_links': generate_links(new_product['id'])
    }), 201

@app.route('/api/products/<int:product_id>', methods=['PUT'])
def update_product(product_id):
    """Produkt aktualisieren"""
    product = next((p for p in products if p['id'] == product_id), None)
    
    if not product:
        return jsonify({
            'error': 'Product not found',
            '_links': generate_links()
        }), 404
    
    data = request.get_json()
    
    if not data or 'name' not in data or 'price' not in data:
        return jsonify({
            'error': 'Name and price are required',
            '_links': generate_links(product_id)
        }), 400
    
    product.update({
        'name': data['name'],
        'price': data['price'],
        'category': data.get('category', product['category'])
    })
    
    return jsonify({
        'message': 'Product updated successfully',
        'product': product,
        '_links': generate_links(product_id)
    }), 200

@app.route('/api/products/<int:product_id>', methods=['DELETE'])
def delete_product(product_id):
    """Produkt löschen"""
    global products
    product = next((p for p in products if p['id'] == product_id), None)
    
    if not product:
        return jsonify({
            'error': 'Product not found',
            '_links': generate_links()
        }), 404
    
    products = [p for p in products if p['id'] != product_id]
    
    return jsonify({
        'message': 'Product deleted successfully',
        '_links': generate_links()
    }), 200

if __name__ == '__main__':
    app.run(debug=True)

HTTP коды состояния

2xx Успех

  • 200 OK: запрос выполнен успешно
  • 201 Created: ресурс создан
  • 204 No Content: запрос выполнен успешно, нет данных для возврата

3xx Перенаправление

  • 301 Moved Permanently: постоянное перенаправление
  • 302 Found: временное перенаправление
  • 304 Not Modified: содержимое не изменилось (кэш)

4xx Ошибки клиента

  • 400 Bad Request: некорректный запрос
  • 401 Unauthorized: требуется аутентификация
  • 403 Forbidden: доступ запрещен
  • 404 Not Found: ресурс не найден
  • 409 Conflict: конфликт с существующим состоянием

5xx Ошибки сервера

  • 500 Internal Server Error: внутренняя ошибка сервера
  • 502 Bad Gateway: ошибка шлюза
  • 503 Service Unavailable: сервис недоступен

Richardson Maturity Model

Level 0: Swamp of POX

POST /api/products
{"action": "getAll"}

Level 1: Resources

GET /api/getAllProducts
POST /api/createProduct

Level 2: HTTP Verbs

GET /api/products
POST /api/products
PUT /api/products/123
DELETE /api/products/123

Level 3: Hypermedia (HATEOAS)

{
  "product": {
    "id": 123,
    "name": "Laptop",
    "price": 999.99
  },
  "_links": {
    "self": { "href": "/api/products/123" },
    "update": { "href": "/api/products/123", "method": "PUT" },
    "delete": { "href": "/api/products/123", "method": "DELETE" },
    "products": { "href": "/api/products" }
  }
}

Преимущества и недостатки

Преимущества REST

  • Масштабируемость: stateless архитектура позволяет горизонтальное масштабирование
  • Гибкость: поддержка разных форматов данных (JSON, XML, HTML)
  • Простота: использует известный HTTP протокол
  • Кэшируемость: ответы могут быть закэшированы
  • Разделение: четкое разделение между клиентом и сервером

Недостатки

  • Издержки: HTTP заголовки и структура JSON добавляют объем
  • Statelessness: требует управления состоянием на стороне клиента
  • Версионирование: контроль версий API может быть сложным
  • Безопасность: требует HTTPS и аутентификации

Типовые экзаменационные вопросы

  1. В чем разница между PUT и PATCH? PUT заменяет весь ресурс, PATCH изменяет только его части.

  2. Объясните HATEOAS! Hypermedia As The Engine Of Application State, когда клиент навигирует через ссылки без знания фиксированных URL.

  3. Почему stateless важен для REST? Позволяет горизонтальное масштабирование и упрощает распределение нагрузки.

  4. Что означает идемпотентность HTTP методов? Повторное выполнение приводит к тому же результату (GET, PUT, DELETE).

Основные источники

  1. https://restfulapi.net/
  2. https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
  3. https://www.jsonapi.org/
Назад к блогу
Share:

Похожие статьи