Ir al contenido

Tutorial: DTOs, repositorio y controller de tu API REST

Al terminar este tutorial vas a ser capaz de:

  • Explicar qué es un DTO y por qué lo que el API devuelve no es necesariamente lo que vive en la base de datos.
  • Diseñar la interfaz de un repositorio como contrato entre capas.
  • Traducir tus interfaces TypeScript a clases Java respetando los tipos de datos.
  • Reconocer el mismo patrón de inyección de dependencias que ya conoces de Angular, ahora en Spring.
  • Explicar por qué, en esta primera versión, el Controller llama directamente al Repository, y en qué momento eso va a cambiar.
  • Conectar tu front en Angular con tu propio backend, cambiando una sola URL.

Prerrequisitos: haber completado el tutorial de creación del proyecto Spring Boot y el Release 1 (tienes tus interfaces TypeScript y tu Gist con datos).


DTO significa Data Transfer Object: es la clase Java que describe la forma del JSON que tu API va a devolver. No es necesariamente igual a lo que vive en la base de datos: es el contrato entre tu back y tu front.

El Spring Initializr ya creó la carpeta de tu módulo funcional (la que arma uniendo Group Id y Artifact Id, como viste en el tutorial anterior): ahí, junto a la clase de arranque de la aplicación, van a vivir todas las clases del módulo: DTOs, repositorio, servicio y controlador, todas en el mismo paquete.

src/main/java/co/edu/uniandes/musical/artistas/

Cada DTO se nombra NombreDTO, con DTO en mayúsculas.

2.1 La entidad principal del módulo: ArtistaDTO.java

Sección titulada «2.1 La entidad principal del módulo: ArtistaDTO.java»

Cada módulo funcional gira alrededor de una entidad principal, la que le da nombre al módulo. Acá es Artista, así que empezamos por ahí:

package co.edu.uniandes.musical.artistas;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArtistaDTO {
private Long id;
private String nombre;
private String fotoArtistaUrl;
private String paisOrigen;
private String biografia;
private String fechaNacimiento;
private List<AlbumDTO> albumes;
}

@Data es una anotación de Lombok que genera automáticamente getters, setters, toString, equals y hashCode a partir de los atributos de la clase: te ahorra escribir ese código repetitivo a mano. Le agregamos además @NoArgsConstructor y @AllArgsConstructor porque en el siguiente paso vas a construir estos objetos con todos sus datos de una vez, a partir de lo que traiga tu Gist. Vas a volver a ver estas mismas anotaciones más adelante, en Persistencia con JPA, aplicadas ahora a las entidades que sí se guardan en base de datos.

Dos decisiones que vale la pena notar:

  • id es Long en Java, no number como en TypeScript. En Java los enteros tienen tamaño: int (32 bits) y long (64 bits). Para ids de base de datos se usa Long por convención: es el tipo que JPA va a esperar en el Release 6.
  • Las fechas son String por ahora. Tu Gist trae las fechas como texto ("1969-09-26"). Más adelante en el curso las convertirás a LocalDate cuando las reglas de negocio lo exijan.

2.2 Repite el patrón con las demás entidades de tu módulo

Sección titulada «2.2 Repite el patrón con las demás entidades de tu módulo»

Crea, dentro de la misma carpeta artistas/, una clase por cada una de las demás entidades de tu módulo (en este ejemplo, AlbumDTO y CancionDTO), siguiendo exactamente el mismo patrón: @Data, @NoArgsConstructor, @AllArgsConstructor, y los atributos que ya definiste en tus interfaces de TypeScript del Release 1.

Cuando termines, tu carpeta artistas/ debe verse así:

artistas/
├── ArtistaDTO.java
├── AlbumDTO.java
└── CancionDTO.java

Compila para verificar que no hay errores de sintaxis:

Ventana de terminal
./mvnw compile

Si compila sin errores, los contratos están bien definidos. Continúa al paso 3.


Antes de seguir escribiendo código, vale la pena ver hacia dónde vas. En una arquitectura de tres capas completa, una petición HTTP fluye así:

flowchart LR
    F[Front en Angular] -->|"GET /artistas"| C[Controller]
    C --> S[Service]
    S --> R[["Repository (interfaz)"]]

El Controller recibe la petición HTTP y la traduce a una llamada de método. El Service es donde viven las reglas de negocio. El Repository es el contrato de acceso a datos que ya escribiste.

Hoy tu módulo no tiene ninguna regla de negocio que validar. En el Release 3 vas a agregar tu primer POST /artistas, pero todavía sin reglas de negocio que lo justifiquen: el Service llega en una entrega posterior, cuando el módulo sí necesite validar datos antes de crear un artista. Mientras el Service no existe, el Controller llama directamente al Repository:

flowchart LR
    F[Front en Angular] -->|"GET /artistas"| C[Controller]
    C --> R[["Repository (interfaz)"]]

Con el mapa completo, construyes primero el Repository (porque el Controller va a depender de él) y después el Controller.


Antes de escribir cualquier dato, defines el contrato de acceso a datos: qué operaciones existen y qué devuelven. La implementación concreta (hoy un arreglo en memoria, en el Release 6 una base de datos) viene después.

Dentro de la misma carpeta artistas/, crea el archivo ArtistaRepository.java:

package co.edu.uniandes.musical.artistas;
import java.util.List;
public interface ArtistaRepository {
List<ArtistaDTO> findAll();
}

Como ArtistaRepository y ArtistaDTO quedan en el mismo paquete, no necesitas importar ArtistaDTO: Java solo exige el import cuando la clase vive en un paquete distinto.

Es todo. Una interfaz, un método, un contrato.

El nombre findAll() no es arbitrario: es la convención de Spring Data JPA para “dame todos los registros”. Cuando en el Release 6 reemplaces esta implementación por JPA, el nombre ya va a ser el correcto, sin cambiar nada más.

Compila de nuevo para verificar:

Ventana de terminal
./mvnw compile

5. ¿Tu módulo necesita más interfaces de repositorio?

Sección titulada «5. ¿Tu módulo necesita más interfaces de repositorio?»

ArtistaRepository cubre el acceso a datos de la entidad principal del módulo. Pero si tu módulo tiene otra entidad a la que tu API le da acceso independiente (por ejemplo, porque vas a exponer su propio endpoint, como GET /albumes/{id}), esa entidad necesita su propia interfaz de repositorio, siguiendo exactamente el mismo patrón que acabas de ver: una interfaz, un método por operación que necesites.

En este ejemplo concreto no hace falta: Album y Cancion viajan siempre anidados dentro de ArtistaDTO (son parte de la respuesta de GET /artistas, no recursos con su propio endpoint), así que no necesitan su propio repositorio. Si tu módulo sí tiene un caso así, ya sabes cómo resolverlo. No lo vamos a repetir aquí.

Con las interfaces de tu módulo completas (DTOs y repositorio), sigue al Paso 6: el controlador.


El controlador es la puerta de entrada HTTP de tu módulo: traduce una petición como GET /artistas en una llamada a Java.

Dentro de la misma carpeta artistas/, crea el archivo ArtistaController.java:

package co.edu.uniandes.musical.artistas;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@CrossOrigin(origins = "http://localhost:4200")
public class ArtistaController {
private final ArtistaRepository artistaRepository;
public ArtistaController(ArtistaRepository artistaRepository) {
this.artistaRepository = artistaRepository;
}
@GetMapping("/artistas")
public List<ArtistaDTO> listarArtistas() {
return artistaRepository.findAll();
}
}

Fíjate en dos cosas:

  • El constructor recibe ArtistaRepository, no ArtistaService. Es la simplificación del diagrama anterior: mientras no haya reglas de negocio que validar, el Controller llama directo al Repository. Spring te inyecta automáticamente la implementación, igual que ya viste con inject() en Angular.
  • @CrossOrigin(origins = "http://localhost:4200") es necesaria por el navegador, no por Spring. Tu front en Angular corre en un origen (http://localhost:4200) distinto al de este backend (http://localhost:8080). Sin esta anotación, el navegador bloquea la respuesta aunque el backend haya funcionado perfectamente: es una política de seguridad del navegador (CORS), no un error tuyo.

Compila para verificar:

Ventana de terminal
./mvnw compile

Con esto, el módulo ya compila completo: DTOs, interfaz de repositorio y controlador.


Con InMemoryArtistaRepository ya implementado (ver el tutorial de la implementación en memoria), arranca tu backend:

Ventana de terminal
./mvnw spring-boot:run

Verifica que responde con tus datos, por ejemplo con curl o abriendo la URL en el navegador:

Ventana de terminal
curl http://localhost:8080/artistas

Deberías ver un arreglo JSON con tus artistas, en la forma exacta de ArtistaDTO.

Ahora conecta tu front. Abre el archivo environment de tu proyecto Angular que apunta al Gist (el que creaste en el taller de crear un componente para listar) y reemplaza esa URL por la de tu nuevo backend:

export const environment = {
gistUrl: 'http://localhost:8080/artistas',
};

Corre tu front (ng serve) y recarga la página.


Al final de este tutorial (y del tutorial de la implementación en memoria) tienes:

  • Un backend Spring Boot corriendo en el puerto 8080, que expone GET /artistas.
  • Los DTOs de tu módulo, en artistas/, que reflejan exactamente las interfaces TypeScript de tu módulo.
  • La(s) interfaz(ces) de repositorio de tu módulo, y una implementación fake en memoria.
  • Un controlador que llama directamente al repositorio, todavía sin Service.
  • Tu front en Angular, apuntando a tu propio backend en vez del Gist.

Lo que viene después:

  • Release 3: vas a agregar tu primer POST /artistas, todavía Controller → Repository directo, sin Service ni reglas de negocio.
  • Release 6: vas a reemplazar la implementación en memoria del repositorio por una real con JPA, sin tocar el controlador ni la interfaz.