Diseño de REST API y DTO: un regalo sin envoltorio es peligroso

«¿Eh? ¿Por qué se ve la contraseña?»

Al principio de mi vida laboral, iba y venía entre frontend y backend desarrollando a toda prisa. Un día estaba conectando a la pantalla del frontend la «API de consulta de lista de miembros» que había desarrollado un compañero que entró a la empresa junto conmigo.

Abrí la pestaña Network de las herramientas de desarrollo de Chrome (F12) para comprobar si los datos llegaban bien, y en el momento en que vi la respuesta, dudé de mis propios ojos.

[
  {
    "id": 1,
    "username": "tester",
    "password": "$2a$10$D...", // Contrasena cifrada expuesta
    "ssn": "900101-1...",      // DNI expuesto
    "createdAt": "2024-01-01"
  }
]

Sorprendido, le pregunté: «Oye, ¿por qué en la respuesta de esta API se ven la contraseña y hasta el número de identificación?»

Me respondió con toda calma. «Ah, ¿eso? Total, en la pantalla solo mostramos el nombre. Me dio pereza, así que devolví directamente el resultado de userRepository.findAll(). ¿Hay algún problema?»

Sentí un escalofrío. Mi compañero solo buscaba comodidad, pero en la práctica había sembrado la semilla de un grave incidente de seguridad.

«¡Que no aparezca en pantalla no significa que sea seguro!»

Cualquiera puede abrir la pestaña de red del navegador. ¿Qué pasaría si un atacante malicioso llamara a esta API?

Enviar una entidad tal cual era lo mismo que convertir el dormitorio principal de nuestra casa en una pared de cristal transparente.

Una bolsa transparente que deja ver todo su contenido es, para un ladrón, una invitación en toda regla.

DTO: el ‘envoltorio’ de los datos

Una ‘Entidad(Entity)’ es una ‘materia prima’ que solo se manipula dentro de la fábrica. Lleva pegadas cosas que nunca deberían mostrarse al cliente, como secretos (contraseñas) o información de uso interno (fecha de creación, fecha de modificación, bandera de borrado).

En cambio, un ‘DTO(Data Transfer Object)’ es un ‘producto terminado’, bien empaquetado para poder entregarlo al cliente.

Devolver una entidad directamente es como servirle al cliente, en vez de un filete ya cocinado, un pedazo de carne cruda y ensangrentada sobre el plato. Tenemos que pasar sí o sí por un proceso de ‘mapeo’.

[Code Verification] El pantano del bucle infinito (referencia circular)

Más que los problemas de seguridad, lo que vuelve loco a un desarrollador es el problema de la ‘referencia circular’. ¿Recuerdas el mapeo bidireccional de JPA que vimos la vez pasada, User <-> Team?

¿Qué pasaría si devolvieras la entidad User directamente tras convertirla a JSON?

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne // Un usuario pertenece a un equipo
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team") // Un equipo tiene varios usuarios
    private List<User> users = new ArrayList<>();
}

Si devuelves User en ese estado, el serializador JSON (Jackson) funciona así.

Resultado: el servidor cae heroicamente con un StackOverflowError. Las relaciones entre entidades se persiguen unas a otras sin fin, así que si las expones sin cortar esa cadena, acabas en un bucle infinito. Esa es la razón decisiva para usar un DTO y llevar solo los campos necesarios, como teamName.

Si no cortas la cadena de relaciones con un DTO, los datos giran para siempre.

La condición de una buena API: ser RESTful

Si ya envolviste bien los datos con DTO, ahora toca decidir bien la ‘dirección’ a la que enviar ese paquete. Eso es exactamente el diseño de una REST API.

En la universidad yo ponía nombres a las URL como me daba la gana.

No es buena idea meter verbos como join o update dentro de la URL. En un diseño RESTful, el ‘recurso’ (sustantivo) va en la URL y la ‘acción’ (verbo) se deja al método HTTP.

Si lo diseñas así, con solo ver la URL ya puedes entender: «Ah, aquí se está manejando el recurso usuario». Además, la comunicación con los desarrolladores frontend se vuelve mucho más sencilla. Ese es el acuerdo común que comparten los desarrolladores de todo el mundo.

Consejo práctico: el nombre ya hace la mitad del trabajo (estrategia de nombres para DTO)

A veces abro el código del proyecto actual de la empresa y lo primero que me sale es un suspiro. UserDto, MemberVo, InfoParam, ResultData… los anteriores desarrolladores les pusieron nombres como quisieron, así que no sabes si un objeto recibe una petición o entrega una respuesta hasta que miras el código por dentro.

Un DTO es el ‘recipiente’ de los datos. Si el uso de ese recipiente no aparece en el nombre, aparece el tipo de caos en el que sirves sopa en un cuenco de arroz y agua en un plato de sopa. La regla de nombres que más recomiendo en el trabajo real es la siguiente.

public class UserDto {
    // Solicitud de registro
    public static class SignUpRequest {
        private String email;
        private String password;
    }

    // Respuesta con info del usuario
    public static class Response {
        private String name;
        private String email;
    }
}

Cerrando la serie 2: la casa ya está construida

Con esto termina la serie [Monolith Builder]. Sentamos las bases con Spring Boot (DI), separamos frontend y backend (CORS), aprendimos a trabajar con la base de datos mediante JPA y también aprendimos a comunicarnos con DTO y REST API.

Ahora ya tenemos la capacidad de crear por nuestra cuenta un servicio web bastante serio. Pero el viaje de un desarrollador no termina aquí. ¿De qué sirve que el servicio que hice funcione solo en mi ordenador, en localhost? Para mostrárselo a usuarios reales, tiene que subirse a un servidor.

Pero ese servidor no es Windows, sino Linux, una pantalla negra y nada más. Sin ratón, ¿cómo instalas programas y los ejecutas allí?

En la siguiente serie, [Works on My Machine], entremos en el mundo de Linux y Docker para resolver el problema eterno de los desarrolladores: «En mi ordenador funciona, pero en el servidor no».

Deja un comentario