Основы 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
- Значимо для веб-разработки и архитектуры ПО
Основные компоненты
- Ресурсы: уникальные идентификаторы (URI) для данных
- HTTP-методы: CRUD-операции через HTTP-глаголы
- Коды состояния: стандартизированные коды ответа
- Представления: JSON, XML, HTML как форматы данных
- Гипермедиа: ссылки для навигации между ресурсами
- Отсутствие состояния: каждый запрос содержит всю необходимую информацию
- Кэшируемость: ответы можно сохранять
- Многоуровневая система: балансировщики нагрузки, прокси, шлюзы
Примеры на практике
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 и аутентификации
Типовые экзаменационные вопросы
-
В чем разница между PUT и PATCH? PUT заменяет весь ресурс, PATCH изменяет только его части.
-
Объясните HATEOAS! Hypermedia As The Engine Of Application State, когда клиент навигирует через ссылки без знания фиксированных URL.
-
Почему stateless важен для REST? Позволяет горизонтальное масштабирование и упрощает распределение нагрузки.
-
Что означает идемпотентность HTTP методов? Повторное выполнение приводит к тому же результату (GET, PUT, DELETE).
Основные источники
- https://restfulapi.net/
- https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- https://www.jsonapi.org/



