Skip to content
IRC-CodingIRC-Coding
Fundamentos REST APIHTTP Métodos StatuscodesHATEOASRichardson Maturity ModelWeb Services

Fundamentos de REST API: HTTP, Statuscodes y HATEOAS

Aprende los fundamentos de REST API con HTTP, statuscodes, HATEOAS y Richardson Maturity Model. Best practices para Web Services.

S

schutzgeist

15 min read
Fundamentos de REST API: HTTP, Statuscodes y HATEOAS

Fundamentos de REST API: Métodos HTTP, Códigos de Estado, HATEOAS y Modelo de Madurez Richardson

Este artículo es una introducción exhaustiva a los fundamentos de REST API, incluyendo métodos HTTP, códigos de estado, HATEOAS y Modelo de Madurez Richardson con ejemplos prácticos.

En resumen

Las APIs REST utilizan métodos HTTP para operaciones CRUD, códigos de estado estandarizados para resultados, HATEOAS para APIs autodescubribles y siguen el Modelo de Madurez Richardson para diferentes niveles de madurez.

Descripción técnica compacta

REST (Transferencia de Estado Representacional) es un estilo arquitectónico para sistemas distribuidos que utiliza el protocolo HTTP y métodos estándar.

Métodos HTTP:

  • GET: Consultar recurso (seguro, idempotente)
  • POST: Crear recurso (no seguro, no idempotente)
  • PUT: Actualizar/reemplazar recurso (no seguro, idempotente)
  • PATCH: Actualizar recurso parcialmente (no seguro, no idempotente)
  • DELETE: Eliminar recurso (no seguro, idempotente)

Códigos de estado:

  • 2xx: Operaciones exitosas (200, 201, 204)
  • 3xx: Redirecciones (301, 302, 304)
  • 4xx: Errores del cliente (400, 401, 403, 404, 422)
  • 5xx: Errores del servidor (500, 502, 503)

HATEOAS: Hipermedia como Motor del Estado de la Aplicación - las APIs son autodescubribles mediante hipervínculos.

Puntos clave de referencia

  • REST: Estilo arquitectónico para servicios web con HTTP
  • Métodos HTTP: GET, POST, PUT, PATCH, DELETE para CRUD
  • Códigos de estado: Códigos de respuesta estandarizados (2xx, 3xx, 4xx, 5xx)
  • HATEOAS: Navegación basada en hipermedia entre recursos
  • Modelo de Madurez Richardson: Niveles de madurez para APIs REST (0-3)
  • Idempotencia: Múltiples llamadas tienen el mismo efecto
  • Sin estado: Sin estado del lado del servidor
  • Relevante para certificaciones: Arquitectura moderna de servicios web

Componentes principales

  1. Recursos: Entidades identificables con URIs
  2. Métodos HTTP: Operaciones estandarizadas
  3. Códigos de estado: Formato de respuesta uniforme
  4. Representaciones: JSON, XML, HTML, etc.
  5. HATEOAS: Navegación basada en hipermedia
  6. Sin estado: Comunicación sin estado
  7. Cacheabilidad: Encabezados de caché HTTP
  8. Interfaz uniforme: Convenciones de API consistentes

Ejemplos prácticos

1. REST API con Spring Boot (Java)

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

// Modelo de datos
class User {
    private Long id;
    private String name;
    private String email;
    private Map<String, String> _links = new HashMap<>();
    
    public User() {}
    
    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    // Getters y setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Map<String, String> get_links() { return _links; }
    public void set_links(Map<String, String> links) { this._links = links; }
}

// Repositorio (en memoria para demostración)
@Repository
class UserRepository {
    private final Map<Long, User> users = new ConcurrentHashMap<>();
    private long nextId = 1;
    
    public List<User> findAll() {
        return new ArrayList<>(users.values());
    }
    
    public Optional<User> findById(Long id) {
        return Optional.ofNullable(users.get(id));
    }
    
    public User save(User user) {
        if (user.getId() == null) {
            user.setId(nextId++);
        }
        users.put(user.getId(), user);
        return user;
    }
    
    public void deleteById(Long id) {
        users.remove(id);
    }
    
    public boolean existsById(Long id) {
        return users.containsKey(id);
    }
}

// Controlador REST
@RestController
@RequestMapping("/api/users")
class UserController {
    
    private final UserRepository userRepository;
    
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    // GET /api/users - Obtener todos los usuarios
    @GetMapping
    public ResponseEntity<Map<String, Object>> getAllUsers() {
        List<User> users = userRepository.findAll();
        
        // Agregar enlaces HATEOAS
        for (User user : users) {
            addHateoasLinks(user);
        }
        
        Map<String, Object> response = new HashMap<>();
        response.put("users", users);
        response.put("_links", Map.of(
            "self", Map.of("href", "/api/users"),
            "create", Map.of("href", "/api/users", "method", "POST")
        ));
        response.put("count", users.size());
        
        return ResponseEntity.ok(response);
    }
    
    // GET /api/users/{id} - Obtener usuario
    @GetMapping("/{id}")
    public ResponseEntity<?> getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
            .map(user -> {
                addHateoasLinks(user);
                return ResponseEntity.ok(user);
            })
            .orElse(ResponseEntity.notFound().build());
    }
    
    // POST /api/users - Crear usuario
    @PostMapping
    public ResponseEntity<Map<String, Object>> createUser(@RequestBody User user) {
        // Validación
        if (user.getName() == null || user.getName().trim().isEmpty()) {
            return ResponseEntity.badRequest()
                .body(Map.of("error", "El nombre no puede estar vacío"));
        }
        
        if (user.getEmail() == null || !user.getEmail().contains("@")) {
            return ResponseEntity.badRequest()
                .body(Map.of("error", "Dirección de correo electrónico no válida"));
        }
        
        User savedUser = userRepository.save(user);
        addHateoasLinks(savedUser);
        
        Map<String, Object> response = new HashMap<>();
        response.put("user", savedUser);
        response.put("message", "Usuario creado exitosamente");
        response.put("_links", Map.of(
            "self", Map.of("href", "/api/users/" + savedUser.getId()),
            "all", Map.of("href", "/api/users")
        ));
        
        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(response);
    }
    
    // PUT /api/users/{id} - Actualizar usuario completamente
    @PutMapping("/{id}")
    public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User user) {
        if (!userRepository.existsById(id)) {
            return ResponseEntity.notFound().build();
        }
        
        user.setId(id);
        User updatedUser = userRepository.save(user);
        addHateoasLinks(updatedUser);
        
        Map<String, Object> response = new HashMap<>();
        response.put("user", updatedUser);
        response.put("message", "Usuario actualizado exitosamente");
        
        return ResponseEntity.ok(response);
    }
    
    // PATCH /api/users/{id} - Actualizar usuario parcialmente
    @PatchMapping("/{id}")
    public ResponseEntity<?> partialUpdateUser(@PathVariable Long id, 
                                             @RequestBody Map<String, Object> updates) {
        return userRepository.findById(id)
            .map(user -> {
                // Actualizar solo los campos especificados
                if (updates.containsKey("name")) {
                    user.setName((String) updates.get("name"));
                }
                if (updates.containsKey("email")) {
                    user.setEmail((String) updates.get("email"));
                }
                
                User updatedUser = userRepository.save(user);
                addHateoasLinks(updatedUser);
                
                Map<String, Object> response = new HashMap<>();
                response.put("user", updatedUser);
                response.put("message", "Usuario actualizado parcialmente");
                
                return ResponseEntity.ok(response);
            })
            .orElse(ResponseEntity.notFound().build());
    }
    
    // DELETE /api/users/{id} - Eliminar usuario
    @DeleteMapping("/{id}")
    public ResponseEntity<Map<String, Object>> deleteUser(@PathVariable Long id) {
        if (!userRepository.existsById(id)) {
            return ResponseEntity.notFound().build();
        }
        
        userRepository.deleteById(id);
        
        Map<String, Object> response = new HashMap<>();
        response.put("message", "Usuario eliminado exitosamente");
        response.put("_links", Map.of(
            "all", Map.of("href", "/api/users")
        ));
        
        return ResponseEntity.ok(response);
    }
    
    // Agregar enlaces HATEOAS
    private void addHateoasLinks(User user) {
        Map<String, String> links = new HashMap<>();
        links.put("self", "/api/users/" + user.getId());
        links.put("collection", "/api/users");
        links.put("update", "/api/users/" + user.getId());
        links.put("delete", "/api/users/" + user.getId());
        user.set_links(links);
    }
}

// Manejador de excepciones
@ControllerAdvice
class GlobalExceptionHandler {
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, String>> handleIllegalArgument(IllegalArgumentException e) {
        return ResponseEntity.badRequest()
            .body(Map.of("error", e.getMessage()));
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, String>> handleGenericException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(Map.of("error", "Error interno del servidor"));
    }
}

// Aplicación Spring Boot
@SpringBootApplication
public class RestApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(RestApiApplication.class, args);
    }
    
    @Bean
    public CommandLineRunner initData(UserRepository userRepository) {
        return args -> {
            // Crear datos de prueba
            userRepository.save(new User(null, "Alice", "alice@example.com"));
            userRepository.save(new User(null, "Bob", "bob@example.com"));
            userRepository.save(new User(null, "Charlie", "charlie@example.com"));
            
            System.out.println("Datos de prueba creados");
        };
    }
}

2. Demo del Modelo de Madurez Richardson

// Richardson Maturity Model Level 0: Swamp of POX
class Level0Api {
    private String baseUrl;
    
    public Level0Api(String baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    // Keine HTTP-Methoden, nur POST für alles
    public String createUser(String name, String email) {
        // POST /api - Keine REST-Konventionen
        String payload = String.format("{\"action\": \"create\", \"name\": \"%s\", \"email\": \"%s\"}", 
                                     name, email);
        return postRequest("/api", payload);
    }
    
    public String getUser(long id) {
        // POST /api - Keine URI-Ressourcen
        String payload = String.format("{\"action\": \"get\", \"id\": %d}", id);
        return postRequest("/api", payload);
    }
    
    private String postRequest(String endpoint, String payload) {
        // Simulierter HTTP-Aufruf
        return "POST " + baseUrl + endpoint + " - Body: " + payload;
    }
}

// Richardson Maturity Model Level 1: Resources
class Level1Api {
    private String baseUrl;
    
    public Level1Api(String baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    // Eigene URIs für Ressourcen, aber nur GET
    public String getUser(long id) {
        // GET /api/users/123 - Korrekte URI, aber nur GET
        return getRequest("/api/users/" + id);
    }
    
    public String getAllUsers() {
        return getRequest("/api/users");
    }
    
    // Aber immer noch POST für alles andere
    public String createUser(String name, String email) {
        String payload = String.format("{\"name\": \"%s\", \"email\": \"%s\"}", name, email);
        return postRequest("/api/users", payload);
    }
    
    private String getRequest(String endpoint) {
        return "GET " + baseUrl + endpoint;
    }
    
    private String postRequest(String endpoint, String payload) {
        return "POST " + baseUrl + endpoint + " - Body: " + payload;
    }
}

// Richardson Maturity Model Level 2: HTTP Verbs
class Level2Api {
    private String baseUrl;
    
    public Level2Api(String baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    // Korrekte HTTP-Methoden für CRUD
    public String getUser(long id) {
        return "GET " + baseUrl + "/api/users/" + id;
    }
    
    public String getAllUsers() {
        return "GET " + baseUrl + "/api/users";
    }
    
    public String createUser(String name, String email) {
        String payload = String.format("{\"name\": \"%s\", \"email\": \"%s\"}", name, email);
        return "POST " + baseUrl + "/api/users - Body: " + payload;
    }
    
    public String updateUser(long id, String name, String email) {
        String payload = String.format("{\"name\": \"%s\", \"email\": \"%s\"}", name, email);
        return "PUT " + baseUrl + "/api/users/" + id + " - Body: " + payload;
    }
    
    public String deleteUser(long id) {
        return "DELETE " + baseUrl + "/api/users/" + id;
    }
}

// Richardson Maturity Model Level 3: Hypermedia (HATEOAS)
class Level3Api {
    private String baseUrl;
    
    public Level3Api(String baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    // Vollständige HATEOAS-Implementierung
    public Map<String, Object> getUser(long id) {
        Map<String, Object> user = new HashMap<>();
        user.put("id", id);
        user.put("name", "Alice");
        user.put("email", "alice@example.com");
        
        // HATEOAS Links
        Map<String, Object> links = new HashMap<>();
        links.put("self", Map.of("href", "/api/users/" + id));
        links.put("collection", Map.of("href", "/api/users"));
        links.put("update", Map.of("href", "/api/users/" + id, "method", "PUT"));
        links.put("delete", Map.of("href", "/api/users/" + id, "method", "DELETE"));
        
        user.put("_links", links);
        
        return user;
    }
    
    public Map<String, Object> getAllUsers() {
        List<Map<String, Object>> users = new ArrayList<>();
        
        // Benutzer mit Links
        Map<String, Object> user1 = getUser(1L);
        Map<String, Object> user2 = getUser(2L);
        
        users.add(user1);
        users.add(user2);
        
        Map<String, Object> response = new HashMap<>();
        response.put("users", users);
        
        // Collection Links
        Map<String, Object> links = new HashMap<>();
        links.put("self", Map.of("href", "/api/users"));
        links.put("create", Map.of("href", "/api/users", "method", "POST"));
        links.put("search", Map.of("href", "/api/users/search", "method", "GET"));
        
        response.put("_links", links);
        response.put("count", users.size());
        
        return response;
    }
    
    public Map<String, Object> createUser(String name, String email) {
        // Erstellte Ressource mit Links zurückgeben
        Map<String, Object> createdUser = new HashMap<>();
        createdUser.put("id", 3L);
        createdUser.put("name", name);
        createdUser.put("email", email);
        
        Map<String, Object> links = new HashMap<>();
        links.put("self", Map.of("href", "/api/users/3"));
        links.put("collection", Map.of("href", "/api/users"));
        links.put("update", Map.of("href", "/api/users/3", "method", "PUT"));
        links.put("delete", Map.of("href", "/api/users/3", "method", "DELETE"));
        
        createdUser.put("_links", links);
        
        Map<String, Object> response = new HashMap<>();
        response.put("user", createdUser);
        response.put("message", "Benutzer erstellt");
        response.put("_links", Map.of(
            "self", Map.of("href", "/api/users/3"),
            "all", Map.of("href", "/api/users")
        ));
        
        return response;
    }
}

// Richardson Maturity Model Demo
public class RichardsonMaturityDemo {
    
    public static void main(String[] args) {
        System.out.println("=== Richardson Maturity Model Demo ===");
        
        String baseUrl = "http://api.example.com";
        
        // Level 0: Swamp of POX
        System.out.println("\n--- Level 0: Swamp of POX ---");
        Level0Api level0 = new Level0Api(baseUrl);
        
        System.out.println("Create User: " + level0.createUser("Alice", "alice@example.com"));
        System.out.println("Get User: " + level0.getUser(123));
        
        // Level 1: Resources
        System.out.println("\n--- Level 1: Resources ---");
        Level1Api level1 = new Level1Api(baseUrl);
        
        System.out.println("Get User: " + level1.getUser(123));
        System.out.println("Get All Users: " + level1.getAllUsers());
        System.out.println("Create User: " + level1.createUser("Bob", "bob@example.com"));
        
        // Level 2: HTTP Verbs
        System.out.println("\n--- Level 2: HTTP Verbs ---");
        Level2Api level2 = new Level2Api(baseUrl);
        
        System.out.println("Get User: " + level2.getUser(123));
        System.out.println("Create User: " + level2.createUser("Charlie", "charlie@example.com"));
        System.out.println("Update User: " + level2.updateUser(123, "Charlie Updated", "charlie.new@example.com"));
        System.out.println("Delete User: " + level2.deleteUser(123));
        
        // Level 3: Hypermedia (HATEOAS)
        System.out.println("\n--- Level 3: Hypermedia (HATEOAS) ---");
        Level3Api level3 = new Level3Api(baseUrl);
        
        Map<String, Object> userResponse = level3.getUser(123);
        System.out.println("Get User with HATEOAS:");
        printJson(userResponse);
        
        Map<String, Object> allUsersResponse = level3.getAllUsers();
        System.out.println("\nAll Users with HATEOAS:");
        printJson(allUsersResponse);
        
        Map<String, Object> createResponse = level3.createUser("David", "david@example.com");
        System.out.println("\nCreate User with HATEOAS:");
        printJson(createResponse);
        
        // Richardson Maturity Analysis
        System.out.println("\n=== Richardson Maturity Analysis ===");
        analyzeMaturityLevel();
    }
    
    private static void printJson(Map<String, Object> data) {
        System.out.println(jsonToString(data, 0));
    }
    
    private static String jsonToString(Object obj, int indent) {
        if (obj instanceof Map) {
            StringBuilder sb = new StringBuilder();
            Map<?, ?> map = (Map<?, ?>) obj;
            String indentStr = "  ".repeat(indent);
            
            sb.append("{\n");
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                sb.append(indentStr).append("\"").append(entry.getKey()).append("\": ");
                sb.append(jsonToString(entry.getValue(), indent + 1));
                sb.append(",\n");
            }
            if (!map.isEmpty()) {
                sb.setLength(sb.length() - 2); // Remove last comma and newline
                sb.append("\n");
            }
            sb.append("  ".repeat(indent - 1)).append("}");
            return sb.toString();
        } else if (obj instanceof List) {
            StringBuilder sb = new StringBuilder();
            List<?> list = (List<?>) obj;
            String indentStr = "  ".repeat(indent);
            
            sb.append("[\n");
            for (Object item : list) {
                sb.append(indentStr).append(jsonToString(item, indent + 1));
                sb.append(",\n");
            }
            if (!list.isEmpty()) {
                sb.setLength(sb.length() - 2);
                sb.append("\n");
            }
            sb.append("  ".repeat(indent - 1)).append("]");
            return sb.toString();
        } else {
            return "\"" + obj + "\"";
        }
    }
    
    private static void analyzeMaturityLevel() {
        System.out.println("Richardson Maturity Model Levels:");
        System.out.println("Level 0 - Swamp of POX: Nur HTTP, keine REST-Konventionen");
        System.out.println("Level 1 - Resources: Eigene URIs, aber nur GET");
        System.out.println("Level 2 - HTTP Verbs: Korrekte HTTP-Methoden");
        System.out.println("Level 3 - Hypermedia: HATEOAS für discoverable APIs");
        
        System.out.println("\nVorteile höherer Levels:");
        System.out.println("- Bessere Cachebarkeit");
        System.out.println("- Klarere Semantik");
        System.out.println("- Entkopplung von Client und Server");
        System.out.println("- Self-describing APIs");
    }
}

3. Cliente JavaScript para REST API

// REST API Client mit HATEOAS-Unterstützung
class RestClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    // Universelle Request-Methode
    async request(method, endpoint, data = null) {
        const config = {
            method: method,
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            }
        };
        
        if (data) {
            config.body = JSON.stringify(data);
        }
        
        try {
            const response = await fetch(this.baseUrl + endpoint, config);
            
            if (!response.ok) {
                const error = await response.json();
                throw new Error(error.error || `HTTP ${response.status}`);
            }
            
            return await response.json();
        } catch (error) {
            console.error('API Error:', error);
            throw error;
        }
    }
    
    // CRUD-Operationen
    async getAllUsers() {
        return this.request('GET', '/users');
    }
    
    async getUser(id) {
        return this.request('GET', `/users/${id}`);
    }
    
    async createUser(userData) {
        return this.request('POST', '/users', userData);
    }
    
    async updateUser(id, userData) {
        return this.request('PUT', `/users/${id}`, userData);
    }
    
    async partialUpdateUser(id, updates) {
        return this.request('PATCH', `/users/${id}`, updates);
    }
    
    async deleteUser(id) {
        return this.request('DELETE', `/users/${id}`);
    }
    
    // HATEOAS-Navigation
    async followLink(resource, linkName) {
        const links = resource._links || {};
        const link = links[linkName];
        
        if (!link) {
            throw new Error(`Link '${linkName}' nicht gefunden`);
        }
        
        const href = link.href;
        const method = link.method || 'GET';
        
        // Absolute URLs behandeln
        const url = href.startsWith('http') ? href : this.baseUrl + href;
        
        const config = {
            method: method,
            headers: {
                'Accept': 'application/json'
            }
        };
        
        const response = await fetch(url, config);
        return response.json();
    }
    
    // Discoverable API-Client
    async discoverApi() {
        try {
            const root = await this.request('GET', '/');
            console.log('API-Entdeckung:', root);
            return root;
        } catch (error) {
            console.warn('API-Entdeckung fehlgeschlagen:', error);
            return null;
        }
    }
}

// HATEOAS-fähiger Client
class HateoasClient {
    constructor(baseUrl) {
        this.restClient = new RestClient(baseUrl);
        this.cache = new Map();
    }
    
    async getUserWithNavigation(id) {
        const user = await this.restClient.getUser(id);
        console.log('Benutzer:', user);
        
        // Verfügbare Aktionen anzeigen
        if (user._links) {
            console.log('Verfügbare Aktionen:');
            Object.keys(user._links).forEach(linkName => {
                const link = user._links[linkName];
                console.log(`  ${linkName}: ${link.href} (${link.method || 'GET'})`);
            });
        }
        
        return user;
    }
    
    async navigateToCollection(resource) {
        try {
            const collection = await this.restClient.followLink(resource, 'collection');
            console.log('Collection:', collection);
            return collection;
        } catch (error) {
            console.error('Navigation zur Collection fehlgeschlagen:', error);
            return null;
        }
    }
    
    async performAction(resource, actionName, data = null) {
        try {
            const link = resource._links[actionName];
            if (!link) {
                throw new Error(`Aktion '${actionName}' nicht verfügbar`);
            }
            
            const method = link.method || 'POST';
            const endpoint = link.href.replace(this.restClient.baseUrl, '');
            
            return await this.restClient.request(method, endpoint, data);
        } catch (error) {
            console.error(`Aktion '${actionName}' fehlgeschlagen:`, error);
            throw error;
        }
    }
}

// REST API Demo
async function restApiDemo() {
    console.log('=== REST API Client Demo ===');
    
    const client = new RestClient('http://localhost:8080/api');
    const hateoasClient = new HateoasClient('http://localhost:8080/api');
    
    try {
        // API-Entdeckung
        console.log('\n--- API-Entdeckung ---');
        const apiInfo = await client.discoverApi();
        
        // Alle Benutzer abrufen
        console.log('\n--- Alle Benutzer ---');
        const users = await client.getAllUsers();
        console.log('Benutzer:', users);
        
        if (users.users && users.users.length > 0) {
            const firstUser = users.users[0];
            
            // Benutzer mit HATEOAS-Navigation
            console.log('\n--- Benutzer mit HATEOAS ---');
            const userWithNav = await hateoasClient.getUserWithNavigation(firstUser.id);
            
            // Zur Collection navigieren
            console.log('\n--- Navigation zur Collection ---');
            const collection = await hateoasClient.navigateToCollection(userWithNav);
            
            // Benutzer aktualisieren
            console.log('\n--- Benutzer aktualisieren ---');
            const updatedUser = await client.updateUser(firstUser.id, {
                name: 'Updated Name',
                email: 'updated@example.com'
            });
            console.log('Aktualisierter Benutzer:', updatedUser);
        }
        
        // Neuen Benutzer erstellen
        console.log('\n--- Benutzer erstellen ---');
        const newUser = await client.createUser({
            name: 'New User',
            email: 'newuser@example.com'
        });
        console.log('Neuer Benutzer:', newUser);
        
        // HATEOAS-Aktionen ausführen
        if (newUser.user && newUser.user._links) {
            console.log('\n--- HATEOAS-Aktionen ---');
            
            // Self-Link folgen
            const selfUser = await hateoasClient.performAction(newUser.user, 'self');
            console.log('Self-Link Ergebnis:', selfUser);
            
            // Collection-Link folgen
            const collection = await hateoasClient.performAction(newUser.user, 'collection');
            console.log('Collection-Link Ergebnis:', collection);
        }
        
    } catch (error) {
        console.error('Demo fehlgeschlagen:', error);
    }
}

// Error Handling und Retry-Mechanismus
class RobustRestClient extends RestClient {
    constructor(baseUrl, maxRetries = 3) {
        super(baseUrl);
        this.maxRetries = maxRetries;
    }
    
    async requestWithRetry(method, endpoint, data = null) {
        let lastError;
        
        for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
            try {
                return await this.request(method, endpoint, data);
            } catch (error) {
                lastError = error;
                
                // Retry nur bei Netzwerkfehlern oder 5xx
                if (!this.shouldRetry(error)) {
                    throw error;
                }
                
                const delay = Math.pow(2, attempt) * 1000; // Exponential Backoff
                console.warn(`Versuch ${attempt} fehlgeschlagen, retry in ${delay}ms:`, error.message);
                
                if (attempt < this.maxRetries) {
                    await this.sleep(delay);
                }
            }
        }
        
        throw lastError;
    }
    
    shouldRetry(error) {
        // Retry bei Netzwerkfehlern oder 5xx Statuscodes
        return error.message.includes('fetch') || 
               error.message.startsWith('HTTP 5');
    }
    
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

// Demo ausführen
if (typeof module !== 'undefined' && module.exports) {
    module.exports = { RestClient, HateoasClient, RobustRestClient, restApiDemo };
} else {
    // Browser-Umgebung
    restApiDemo();
}

Vista general de métodos HTTP

MétodoPropósitoIdempotenteSeguroEjemplos
GETObtener recursoGET /users
POSTCrear recursoNoNoPOST /users
PUTReemplazar recursoNoPUT /users/123
PATCHActualizar recursoNoNoPATCH /users/123
DELETEEliminar recursoNoDELETE /users/123

Vista general de códigos de estado HTTP

2xx Éxito

  • 200 OK: Solicitud exitosa
  • 201 Created: Recurso creado
  • 204 No Content: Exitosa, sin contenido devuelto

3xx Redirección

  • 301 Moved Permanently: Redirección permanente
  • 302 Found: Redirección temporal
  • 304 Not Modified: Sin cambios (caché)

4xx Error del cliente

  • 400 Bad Request: Solicitud inválida
  • 401 Unauthorized: Autenticación requerida
  • 403 Forbidden: Acceso denegado
  • 404 Not Found: Recurso no encontrado
  • 422 Unprocessable Entity: Error de validación

5xx Error del servidor

  • 500 Internal Server Error: Error del servidor
  • 502 Bad Gateway: Error de puerta de enlace
  • 503 Service Unavailable: Servicio no disponible

Modelo de Madurez de Richardson

Nivel 0: Pantano de POX

  • HTTP solo como protocolo de transporte
  • Sin convenciones REST
  • POST para todo

Nivel 1: Recursos

  • URIs propias para cada recurso
  • Solo método GET
  • Sin métodos HTTP correctos

Nivel 2: Verbos HTTP

  • Métodos HTTP correctos
  • Operaciones CRUD
  • Códigos de estado usados correctamente

Nivel 3: Hipermedia (HATEOAS)

  • Todos los niveles anteriores
  • Enlaces de hipermedia
  • APIs autodescriptivas

Mejores prácticas de HATEOAS

Estructura de enlaces

{
  "_links": {
    "self": {
      "href": "/api/users/123"
    },
    "collection": {
      "href": "/api/users"
    },
    "update": {
      "href": "/api/users/123",
      "method": "PUT"
    },
    "delete": {
      "href": "/api/users/123",
      "method": "DELETE"
    }
  }
}

Recursos embebidos

{
  "user": {
    "id": 123,
    "name": "Alice",
    "_embedded": {
      "orders": [
        {
          "id": 456,
          "total": 99.99
        }
      ]
    }
  }
}

Directrices de diseño de API REST

Diseño de URIs

  • Sustantivos: Recursos como sustantivos (/users, /products)
  • Plural: Colecciones en plural (/users no /user)
  • Jerárquico: Estructura lógica (/users/123/orders)
  • Minúsculas: Capitalización consistente

Request/Response

  • JSON: Formato estándar
  • Consistencia: Estructura uniforme
  • Versionado: Versiones de API (/api/v1/users)
  • Paginación: Dividir grandes conjuntos de datos

Seguridad

  • HTTPS: Conexión encriptada
  • Autenticación: JWT, OAuth 2.0
  • Autorización: Derechos basados en roles
  • Rate Limiting: Prevenir abuso

Ventajas y desventajas

Ventajas de REST

  • Escalabilidad: Arquitectura sin estado
  • Flexibilidad: Independiente de plataforma
  • Almacenamiento en caché: Usar caché HTTP
  • Simplicidad: Conceptos simples
  • Estandarización: Estándar HTTP

Desventajas

  • Sobrecarga: Sobrecarga de encabezados HTTP
  • Sin estado: Gestión manual del estado
  • Complejidad: HATEOAS puede ser complejo
  • Versionado: Versionamiento de API desafiante

Preguntas frecuentes de examen

  1. ¿Cuál es la diferencia entre PUT y PATCH? PUT reemplaza el recurso completo, PATCH actualiza solo partes.

  2. ¡Explica HATEOAS! Hipermedia as the Engine of Application State - las APIs son autodescubribles a través de hipervínculos.

  3. ¿Qué significa idempotente en métodos HTTP? Múltiples llamadas tienen el mismo efecto que una sola llamada.

  4. ¿Qué niveles existen en el Modelo de Madurez de Richardson? Nivel 0 (POX), Nivel 1 (Recursos), Nivel 2 (Verbos HTTP), Nivel 3 (Hipermedia).

Fuentes principales

  1. https://restfulapi.net/
  2. https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
  3. https://martinfowler.com/articles/richardsonMaturityModel.html
Volver al blog
Share:

Entradas relacionadas