«¿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?
- Filtración de datos personales: se roban los números de identificación y de teléfono de todos los miembros.
- Robo de cuentas: incluso una contraseña cifrada puede servir de pista para un ataque de fuerza bruta (Brute Force).
- Exposición de la lógica interna: a partir de campos como
createdAtocreatedByse puede deducir la estructura de la base de datos o el patrón de los ID de administrador.
Enviar una entidad tal cual era lo mismo que convertir el dormitorio principal de nuestra casa en una pared de cristal transparente.

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.
- Entity:
User(id, password, name, ssn, createdAt, createdBy…) -> de uso interno en la fábrica - DTO:
UserResponse(id, name) -> listo para el reparto
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í.
- Desenvuelve User. -> ¿Anda, dentro hay un Team?
- Desenvuelve Team. -> ¿Anda, dentro hay Users?
- Desenvuelve el primer User de la lista Users. -> ¿Anda, dentro hay un Team?
- Desenvuelve Team. -> ¿Anda, dentro hay Users?
- (Y así hasta el infinito…)
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.

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.
- Registro de miembro:
/joinUser - Modificación de miembro:
/updateUser - Eliminación de miembro:
/deleteUser
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.
- POST
/users: registro de miembro (créalo) - GET
/users/1: consulta del miembro 1 (tráelo) - PUT
/users/1: modificación del miembro 1 (reemplázalo) - DELETE
/users/1: eliminación del miembro 1 (bórralo)
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.
- Indica su función con un sufijo (Suffix)
- Petición (Request):
Acción+Dominio+Request- Ejemplo:
JoinUserRequest,UpdateUserRequest
- Ejemplo:
- Respuesta (Response):
Dominio+Response- Ejemplo:
UserResponse,UserListResponse
- Ejemplo:
- Si lo llamas simplemente
UserDto, no queda claro si se usa para el registro o para la consulta, y acabas teniendo que abrir el archivo cada vez.
- Petición (Request):
- Agrúpalos con clases internas (Inner Class) Si no te gusta que se multipliquen los archivos, te recomiendo encarecidamente la estrategia de las ‘clases internas’. Consiste en crear una sola clase llamada
UserDtoy reunir dentro de ella los DTO relacionados comostatic class. Así, en el código escribirásUserDto.SignUpRequest, y su pertenencia queda clara: «Ah, es un DTO relacionado con los usuarios».
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».


