Tutorial: DTOs, repositorio y controller de tu API REST
1. Objetivos de aprendizaje
Sección titulada «1. Objetivos de aprendizaje»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).
2. Escribir los DTOs
Sección titulada «2. Escribir los DTOs»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@AllArgsConstructorpublic 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:
idesLongen Java, nonumbercomo en TypeScript. En Java los enteros tienen tamaño:int(32 bits) ylong(64 bits). Para ids de base de datos se usaLongpor convención: es el tipo que JPA va a esperar en el Release 6.- Las fechas son
Stringpor ahora. Tu Gist trae las fechas como texto ("1969-09-26"). Más adelante en el curso las convertirás aLocalDatecuando 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.javaCompila para verificar que no hay errores de sintaxis:
./mvnw compileSi compila sin errores, los contratos están bien definidos. Continúa al paso 3.
3. Cómo van a encajar las piezas
Sección titulada «3. Cómo van a encajar las piezas»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.
4. Escribir la interfaz del repositorio
Sección titulada «4. Escribir la interfaz del repositorio»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:
./mvnw compile5. ¿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.
6. Escribir el controlador
Sección titulada «6. Escribir 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, noArtistaService. 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 coninject()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:
./mvnw compileCon esto, el módulo ya compila completo: DTOs, interfaz de repositorio y controlador.
7. Conectar con tu front
Sección titulada «7. Conectar con tu front»Con InMemoryArtistaRepository ya implementado (ver el
tutorial de la implementación en memoria),
arranca tu backend:
./mvnw spring-boot:runVerifica que responde con tus datos, por ejemplo con curl o abriendo la URL
en el navegador:
curl http://localhost:8080/artistasDeberí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.
¿Dónde quedaste?
Sección titulada «¿Dónde quedaste?»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.