Saltar a contenido

Semana 3 - Patrones de Diseño: MVC y su aplicación en una API REST con Spring Boot

Resumen ejecutivo

El patrón Modelo-Vista-Controlador (MVC) separa responsabilidades en tres capas:

  • Modelo → datos y lógica de negocio.
  • Vista → representación al usuario (JSON, HTML, XML, etc.).
  • Controlador → orquesta las peticiones y respuestas.

En una API REST con Spring Boot la Vista se reduce a los DTO/JSON y los Controladores REST (@RestController) gestionan el flujo.
A continuación veremos la teoría detallada con diagramas, código y buenas prácticas.


1. ¿Qué es un patrón de diseño?

Definición

Un patrón de diseño es una solución reutilizable a un problema común en el diseño de software.

  • No es código terminado, es una receta.
  • Mejora la mantenibilidad, escalabilidad y claridad del sistema.

2. Patrón MVC (Modelo-Vista-Controlador)

2.1 Origen y propósito

Año Autor Contexto
1979 Trygve Reenskaug Interfaces de usuario en Smalltalk-80
Hoy Comunidad Aplicaciones web, móviles, APIs REST

Objetivo

Separar la interfaz de usuario (Vista) de la lógica de negocio (Modelo) y del flujo de control (Controlador).

2.2 Diagrama conceptual

graph TD
    subgraph "Aplicación MVC"
        U[Usuario]
        V[Vista]
        C[Controlador]
        M[Modelo]
        DB[(Base de datos)]
    end
    U -->|interacción| V
    V -->|evento| C
    C -->|actualiza estado| M
    M -->|notifica cambios| V
    M -->|persiste| DB
    style C fill:#ffaaaa
    style M fill:#aaffaa
    style V fill:#aaaaff

2.3 Responsabilidades de cada capa

Capa Responsabilidades clave Ejemplo en API REST
Modelo Entidades JPA, repositorios, servicios, reglas de negocio, validaciones. Libro, LibroRepository, LibroService
Vista Representación del recurso: JSON, XML, PDF, etc. LibroDTO, LibroResponse
Controlador Recibe peticiones HTTP, llama al servicio, devuelve respuesta. LibroController (@RestController)

En las APIs REST la Vista NO es HTML, es el JSON serializado que Spring genera automáticamente.


3. MVC dentro de una API REST con Spring Boot

3.1 Arquitectura de paquetes recomendada

com.miempresa.apirest
├── controller        # ← Capa Controlador
├── service           # ← Lógica de negocio
├── repository        # ← Acceso a datos (JPA)
├── model             # ← Entidades JPA (Modelo)
├── dto               # ← Objetos de transferencia (Vista)
└── config            # ← Configuraciones globales

3.2 Flujo de una petición REST con MVC

sequenceDiagram
    participant Cliente
    participant Controller
    participant Service
    participant Repository
    participant DB

    Cliente->>Controller: GET /api/v1/libros/1
    Controller->>Service: obtenerLibro(1)
    Service->>Repository: findById(1)
    Repository->>DB: SELECT * FROM libro WHERE id=1
    DB-->>Repository: libro entity
    Repository-->>Service: Libro
    Service-->>Controller: LibroDTO
    Controller-->>Cliente: 200 OK + JSON

Nota

La conversión de Libro (entidad) a LibroDTO se realiza normalmente con MapStruct o un mapper manual.


4. Implementación paso a paso en Spring Boot

4.1 Entidad (Modelo)

package com.miempresa.apirest.model;

import jakarta.persistence.*;

@Entity
@Table(name = "libros")
public class Libro {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String titulo;
    private String autor;
    private Integer anio;

    /* getters y setters */
}

4.2 Repositorio (Modelo)

package com.miempresa.apirest.repository;

import com.miempresa.apirest.model.Libro;
import org.springframework.data.jpa.repository.JpaRepository;

public interface LibroRepository extends JpaRepository<Libro, Long> {
    // CRUD ya disponible: findAll, findById, save, deleteById...
}

4.3 DTO (Vista)

package com.miempresa.apirest.dto;

public record LibroDTO(
        Long id,
        String titulo,
        String autor,
        Integer anio
) {}

4.4 Servicio (Modelo + lógica)

package com.miempresa.apirest.service;

import com.miempresa.apirest.dto.LibroDTO;
import com.miempresa.apirest.model.Libro;
import com.miempresa.apirest.repository.LibroRepository;
import org.springframework.stereotype.Service;

@Service
public class LibroService {
    private final LibroRepository repo;

    public LibroService(LibroRepository repo) {
        this.repo = repo;
    }

    public LibroDTO obtenerPorId(Long id) {
        return repo.findById(id)
                   .map(this::toDto)
                   .orElseThrow(() -> new LibroNoEncontradoException(id));
    }

    private LibroDTO toDto(Libro l) {
        return new LibroDTO(l.getId(), l.getTitulo(), l.getAutor(), l.getAnio());
    }
}

4.5 Controlador REST (Controlador)

package com.miempresa.apirest.controller;

import com.miempresa.apirest.dto.LibroDTO;
import com.miempresa.apirest.service.LibroService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/libros")
public class LibroController {

    private final LibroService service;

    public LibroController(LibroService service) {
        this.service = service;
    }

    @GetMapping("/{id}")
    public ResponseEntity<LibroDTO> obtener(@PathVariable Long id) {
        LibroDTO dto = service.obtenerPorId(id);
        return ResponseEntity.ok(dto);
    }

    @PostMapping
    public ResponseEntity<LibroDTO> crear(@RequestBody LibroDTO dto) {
        LibroDTO nuevo = service.crear(dto);
        return ResponseEntity
                .created(URI.create("/api/v1/libros/" + nuevo.id()))
                .body(nuevo);
    }

    @PutMapping("/{id}")
    public LibroDTO actualizar(@PathVariable Long id, @RequestBody LibroDTO dto) {
        return service.actualizar(id, dto);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> eliminar(@PathVariable Long id) {
        service.eliminar(id);
        return ResponseEntity.noContent().build();
    }
}

5. Diagrama de clases simplificado

classDiagram
    class Libro {
        +Long id
        +String titulo
        +String autor
        +Integer anio
    }

    class LibroRepository {
        <<interface>>
    }

    class LibroService {
        -LibroRepository repo
        +LibroDTO obtenerPorId(Long)
        +LibroDTO crear(LibroDTO)
    }

    class LibroController {
        -LibroService service
        +ResponseEntity~LibroDTO~ obtener(Long)
        +ResponseEntity~LibroDTO~ crear(LibroDTO)
    }

    class LibroDTO {
        +Long id
        +String titulo
        +String autor
        +Integer anio
    }

    LibroController --> LibroService
    LibroService --> LibroRepository
    LibroService --> LibroDTO
    LibroRepository --> Libro

6. Buenas prácticas y errores comunes

Buena práctica Ejemplo concreto
No exponer entidades JPA Usar siempre DTOs (LibroDTO).
Validaciones @NotBlank, @Size, @Valid en el DTO.
Manejo global de excepciones @ControllerAdvice + @ExceptionHandler.
Paginación Pageable de Spring Data.
Logging SLF4J + @Slf4j (Lombok).
Tests @WebMvcTest, @DataJpaTest, Testcontainers.

Errores comunes

  • Devolver la entidad JPA directamente → exposición de datos sensibles o lazily loaded.
  • Colocar lógica de negocio en el controlador → acoplamiento y dificultad de test.
  • Ignorar el versionado de API → romper a los clientes antiguos.

7. Extensión: MVC vs MVVM vs MVP

Patrón Vista Intermediario Caso de uso típico
MVC HTML/JSON Controlador APIs REST, monolitos web
MVP Android View Presenter Android nativo
MVVM WPF/Angular ViewModel SPA reactivas, WPF

8. Recursos adicionales