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.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
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 algo:

  • 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.

6.1 Permitir que tu front lea la respuesta: CorsConfig.java

Sección titulada «6.1 Permitir que tu front lea la respuesta: CorsConfig.java»

Tu front en Angular corre en http://localhost:4200 y este backend en http://localhost:8080. Para el navegador son orígenes distintos, y por seguridad no le entrega al front la respuesta de otro origen a menos que el backend declare que lo permite. Es una regla del navegador, no un error tuyo. El backend ya respondió bien, pero tu front no vería nada.

En la misma carpeta artistas/, crea el archivo CorsConfig.java:

package co.edu.uniandes.musical.artistas;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:4200")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
}

Fíjate en tres cosas:

  • Es una sola clase para todo el backend. addMapping("/**") cubre todas las rutas, así que los controllers que escribas después quedan cubiertos sin tocarlos.
  • allowedMethods ya incluye POST, PUT y DELETE, aunque en este release solo tienes GET. Los vas a necesitar en los releases siguientes y así no tendrás que volver a abrir esta clase. Si lo omites, Spring permite solo GET, HEAD y POST, y editar y borrar fallarían.
  • Va en la misma carpeta que el controller. Spring solo encuentra las clases de configuración en el paquete de la aplicación y sus subpaquetes. Si la pones en otro paquete, el backend arranca sin errores pero se comporta como si no tuvieras CORS. Más adelante, cuando integren los módulos, esta clase pasa a una carpeta config/ compartida por todo el grupo (lo verás en el Release 6).

Para entender el porqué con más detalle, incluida la petición previa que envía el navegador, lee la sección de CORS en la teoría del API REST.

Compila para verificar:

Ventana de terminal
./mvnw compile

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


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.
  • Una clase CorsConfig que permite a tu front, en el puerto 4200, leer las respuestas del backend.
  • 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.