Fundamentos de REST API: métodos HTTP, códigos de estado y HATEOAS
Este artículo ofrece una explicación exhaustiva de los fundamentos de REST API, incluyendo métodos HTTP, códigos de estado y principios HATEOAS.
En resumen
REST es un estilo arquitectónico para sistemas distribuidos que utiliza métodos HTTP, códigos de estado y HATEOAS para crear servicios web escalables y sin estado.
Descripción técnica compacta
Representational State Transfer (REST) es un estilo arquitectónico para servicios web definido por Roy Fielding. REST aprovecha la semántica de HTTP para realizar operaciones sobre recursos.
Principios fundamentales:
- Client-Server: separación de responsabilidades
- Stateless: sin estados de sesión en el lado del servidor
- Cacheable: las respuestas pueden almacenarse en caché
- Uniform Interface: interfaz uniforme a través de HTTP
- Layered System: capas intermedias permitidas
- Code on Demand: opcional, el servidor puede enviar código al cliente
Métodos HTTP:
- GET: lectura de recursos (seguro, idempotente)
- POST: creación de recursos (no seguro, no idempotente)
- PUT: reemplazo completo de recursos (no seguro, idempotente)
- PATCH: modificación parcial de recursos (no seguro, no idempotente)
- DELETE: eliminación de recursos (no seguro, idempotente)
HATEOAS (Hypermedia as the Engine of Application State) permite navegar por las APIs sin necesidad de URLs codificadas.
Puntos clave para estudios
- Métodos HTTP: GET, POST, PUT, DELETE con semántica correcta
- Códigos de estado: 2xx (éxito), 3xx (redirección), 4xx (error del cliente), 5xx (error del servidor)
- HATEOAS: hipermedia como control de aplicación
- Stateless: sin estados de sesión en el servidor
- Richardson Maturity Model: modelo de madurez para APIs REST
- Resource Naming: nomenclatura URI consistente
- Relevante para IHK en desarrollo web y arquitectura de software
Componentes principales
- Resources: identificadores únicos (URIs) para datos
- HTTP Methods: operaciones CRUD mediante verbos HTTP
- Status Codes: códigos de respuesta estandarizados
- Representations: JSON, XML, HTML como formatos de datos
- Hypermedia: enlaces para navegación entre recursos
- Statelessness: cada solicitud contiene toda la información necesaria
- Cacheability: las respuestas pueden almacenarse en caché
- Layered System: balanceadores de carga, proxies, gateways
Ejemplos prácticos
1. Diseño de recursos y métodos HTTP
// Ejemplo de REST API con Express.js
const express = require('express');
const app = express();
app.use(express.json());
// Datos (en memoria)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
// GET /users - Lectura de todos los usuarios
app.get('/users', (req, res) => {
res.status(200).json({
users: users,
_links: {
self: { href: '/users' },
create: { href: '/users', method: 'POST' }
}
});
});
// GET /users/{id} - Lectura de un usuario individual
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 - Creación de un nuevo usuario
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} - Reemplazo completo del usuario
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} - Eliminación de usuario
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 con Spring Boot y HATEOAS
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
// GET /api/products - Todos los productos con 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} - Producto individual con 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 - Creación de producto
@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} - Actualización de producto
@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} - Eliminación de producto
@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. REST API con Python Flask
from flask import Flask, jsonify, request, url_for
from werkzeug.exceptions import NotFound, BadRequest
app = Flask(__name__)
# Base de datos en memoria
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):
"""Generar enlaces HATEOAS"""
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():
"""Obtener todos los productos"""
return jsonify({
'products': products,
'_links': generate_links()
}), 200
@app.route('/api/products/<int:product_id>', methods=['GET'])
def get_product(product_id):
"""Obtener un producto específico"""
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():
"""Crear un nuevo producto"""
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):
"""Actualizar un producto"""
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):
"""Eliminar un producto"""
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)
Códigos de estado HTTP
2xx Éxito
- 200 OK: Solicitud exitosa
- 201 Created: Recurso creado
- 204 No Content: Solicitud exitosa, sin contenido
3xx Redirección
- 301 Moved Permanently: Redirección permanente
- 302 Found: Redirección temporal
- 304 Not Modified: Contenido sin cambios (caché)
4xx Errores del cliente
- 400 Bad Request: Solicitud inválida
- 401 Unauthorized: Autenticación requerida
- 403 Forbidden: Acceso denegado
- 404 Not Found: Recurso no encontrado
- 409 Conflict: Conflicto con el estado existente
5xx Errores del servidor
- 500 Internal Server Error: Error del servidor
- 502 Bad Gateway: Error de gateway o proxy
- 503 Service Unavailable: Servicio no disponible
Modelo de madurez de Richardson
Nivel 0: Pantano de POX
POST /api/products
{"action": "getAll"}
Nivel 1: Recursos
GET /api/getAllProducts
POST /api/createProduct
Nivel 2: Verbos HTTP
GET /api/products
POST /api/products
PUT /api/products/123
DELETE /api/products/123
Nivel 3: Hipermedia (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" }
}
}
Ventajas y desventajas
Ventajas de REST
- Escalabilidad: La arquitectura sin estado permite escalado horizontal
- Flexibilidad: Admite varios formatos de datos (JSON, XML, HTML)
- Simplicidad: Utiliza el protocolo HTTP establecido
- Capacidad de caché: Las respuestas pueden almacenarse en caché
- Separación: Distinción clara entre cliente y servidor
Desventajas
- Sobrecarga: Encabezados HTTP y estructura JSON
- Ausencia de estado: Requiere gestión de estado del lado del cliente
- Versionado: El versionado de API puede ser complejo
- Seguridad: HTTPS y autenticación requeridas
Preguntas frecuentes de examen
-
¿Cuál es la diferencia entre PUT y PATCH? PUT reemplaza el recurso completo, mientras que PATCH solo modifica partes del recurso.
-
¡Explica HATEOAS! Hipermedia como motor de estado de aplicación: los clientes navegan mediante enlaces sin URLs fijas.
-
¿Por qué es importante la ausencia de estado para REST? Permite escalado horizontal y simplifica la distribución de carga.
-
¿Qué significa idempotente en los métodos HTTP? Ejecutar múltiples veces produce el mismo resultado (GET, PUT, DELETE).
Recursos más importantes
- https://restfulapi.net/
- https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- https://www.jsonapi.org/



