Backend: API REST que devuelve ids en vez de objetos (Hateoas mal)

wuaixd

Tenemos una lista de vídeos. Cada vídeo tiene una categoría y un autor. Tenemos endpoints CRUD para el video, la categoría o el autor.

Mi misión como front es hacer la página de ultimos videos, en la que sale una lista con 10 videos, y en cada video sale el titulo, la categoria y el autor:
"Le corto el pelo a mi novia y sale mal" "Bromas" "Juanito16"

Pues cuando pido los vídeos, me viene el video así:

{
videoId: 1;
titulo: "Le corto el pelo a mi novia";
categoriaId: 3;
autorId: 13213;
}

Y la idea es que luego pida la categoria 3 y el autor 13213 para sacar "categoria.descripcion" y "autor.nombre"

Aquí en concreto usamos angular y con los observables se puede hacer que se pida y se rellene solo, que me parece cojonudo, pero no quita que para listar 10 videos haga 1 (lista de videos) + 10 (categorias) + 10 (autores) = 21 peticiones

Según en cada app se el volumen de los datos y si se que en total hay por ejemplo 20 categorías, las pido todas y lo guardo en memoria y lo voy rellenando de ahí, pero eso no me convence nada porque por un lado me traigo datos que no necesito (si hubiera 200 categorias y necesito 5, o hago 5 peticiones o hago 1 y me traigo 195 datos que no me hacen falta)

Y este es el ejemplo pequeño, en realidad el video tiene como 10 objetos con ids.

Cuando hice cosas con microservicios era un poco de este palo, pero en el gateway se juntaba todo. En otros proyectos usaba graphQL y ahi pides de cada entidad lo que quieras.

Lo que yo les propongo es crear otra entidad que sea "VideoParaListado" en la que venga en vez del autorId, el nombre del autor o el link a su perfil o lo que necesite.

Me gustaría ver opiniones porque igual lo estoy entendiendo yo todo mal pero me cuesta mucho entenderlo, igual hay otra solución o es directamente. A mi me parece que está bastante claro y que es un diseño incorrecto.

1
D

#1 Bueno, si es una API rest pura y dura entiendo que es lo correcto. Lo que pasa es que no hay porque cerrarse a solo REST con endpoints simples de CRUD. Puedes hacer un Read mas complejo que evite esas requests inecesarias pidiendo otros parametros en la query, pero claro, entonces deja de ser solo CRUD y por ende REST. Pero es que al final lo de que sea REST o no es solo un "principio", no es una ley. A lo que me refiero es que definir una API como REST solo estandariza la forma de crear esa API, pero no implica que tu no puedas crear otras vistas o metodos si los organizas y documentas bien, aunque sean metodos que no hagan un Read estandar.

Por ejemplo crear otro metodo de busqueda que implique carga en el servidor y no en el cliente. Pero eso va muy determinado por la plataforma y por los recursos que tu quieras dedicar a ello.

Hay 1000 maneras de controlar y gestionar los recursos que tu asignas a un endpoint en un Backend, que es la unica excusa que se me ocurriria para negar o no aceptar hacer otro metodo que evite complejidad en el Frontend.

No soy programador de Frontend, entonces no entiendo al 100% que es lo mas correcto en Frontend, pero vamos, por la parte de Backend yo no veo pegas a facilitar recursos al Frontend, si al final es mas sencillo hacerlo en el backend. Para mi no hacerlos es poner piedras en el camino porque si.

1
pantocreitor

Yo estoy con Angular igual y cada vez que estoy en una situación de esas me hago un servicio en back para que me devuelva lo que necesito sin tener que hacer tantas llamadas, me parece un poco chapuza vamos.

1 1 respuesta
cabron

no te preocupes, 21 peticiones no son na, solo tienes que navegar por internet con la consola abierta y la pestaña network y ver la media que hace cada web y ya ves que no es pa tanto.

Luego con los ordenadores más potentes de la historia navegar por internet debería ser trivial, y en lugar de eso te consume toda la ram y te pone la cpu al 100%, pero a quien le importa? lo que importa es que alguien se ha leído un artículo en medium que explicaba la forma correcta de hacer un api y tiene que ser así por que es correcto™

6 1 respuesta
wuaixd

#4 Tal cual. Me lo han dicho prácticamente con esas palabras: si buscas verás que la forma correcta es esta, pero sin un razonamiento de que ventajas tiene ni nada parecido

Ranthas

Cuando es una API con cientos de miles de clientes, debes aportar la mayor flexibilidad posible, y es ahí donde HATEOAS tiene sentido; permite a los clientes poder navegar por las distintas entidades de la API y establecer las relaciones entre ellas según sus necesidades; reduces el acoplamiento y las concreciones al máximo y expones un comportamiento donde cualquier cliente, sea cual sea su necesidad, puede cubrirlo porque la API expone unos endpoints lo suficientemente abstractos.

Pero en una aplicación dedicada, dónde el front solo va a consumir X endpoints ya determinados de antemano, diseñar el backend siguiendo HATEOAS es una estupidez supina. Ahorras tiempo y restas complejidad añadiendo endpoints específicos como el que pides, a costa de aumentar el acoplamiento. Aún así, es un coste admisible si, vuelvo a repetir, es una app donde el front sólo necesita consumir X endpoints específicos.

Y como colofón, si el ejemplo que pones en #1 es una respuesta de la API, te voy a dar una mala noticia: eso no es una respuesta de una API REST con HATEOAS, así que enhorabuena, tu equipo de backend está compuesto de mongoloides cuya formación académica seguramente esté basada en 90% en leer posts en medium.

6
B

Eso no es HATEOAS ni de broma :sweat_smile:

Precisamente como comentas, un endpoint que cumpla el paradigma HATEOAS tiene que constar de elementos que sean navegables. Es decir, la idea es que el usuario pueda acceder a cada uno de los elementos que conforman el output con sólo clicar en el enlace proporcionado junto con el resto de información.

Para frameworks como Spring Boot por ejemplo, tienen librerías que automatizan el proceso de añadido de enlaces hipermedia a cada DTO con sólo extender de EntityModel<T>, ya sea trayendo esos links a través de las Entities o directamente desde los DTO antes de salir, según cómo se quiera gestionar internamente su utilidad. Librerías como WebMvcLinkBuilder permiten el añadido de enlaces en función del controlador del que sean llamados. Me cuesta creer que no hay algo similar en el lenguaje/framework en el que lo hayan implementado. Por ejemplo, esto es lo que devuelve mi API:

spoiler

Y si después clico en el enlace de uno de ellos, me lleva a esto:

spoiler

Y ni siquiera me compliqué demasiado implementándolo ya que era a modo de aprendizaje y no iba a tener finalidad alguna, pero se pueden hacer verdaderos inception proporcionando enlaces a las propias colecciones y sus respectivos elementos si se quiere.

Vamos que si te dicen que eso es HATEOAS puro yo soy Rita la cantaora.

1
CricK

Puede ser algo usual en arquitecturas con microservicios con un API Gateway que redirige las peticiones al microservicio concreto del recurso (microservicio de usuarios, microservicio de vídeos, etc)

Puedes proponer que para evitar el exceso de peticiones se puedan obtener varios ids a la vez separados por puntos y comas estilo la API de Stackexchange :

categorias/3;15;230;99
usuarios/132214;3553

Aunque como todo también tiene sus limitaciones.

1
wuaixd

Se que no es HateOAS por eso mismo, me tendrian que dar el id de las categorias y el enlace para pedir las categorias, y aun asi a nivel de front es lioso que siga todos esos enlaces.

Lo de que el endpoint reciba una lista de ids en vez de un id es algo que en mi situación es asquible, igual no en todos los proyectos pero en los que el equipo de back es mas razonable creo que lo conseguiría.

Aún así tengo el problema de que si me vienen 10 resultados, y 9 tienen el id "1" y otro tiene el id "2" de alguna manera ya tengo que hacer un minimo de logica para que me pida en un mismo viaje solo el 1 y el 2, tengo q recorrer todo al menos una vez o solucionarlo de otra manera, pero sigue añadiendo una lógica que veo innecesaria.

Y todavía está el tema de que me vienen siempre X datos en cada resultado, los necesite o no. Si en otra pagina necesito mostrar el titulo del video, me van a seguir viniendo 10 videos con categoriaID que a mi realmente no me hace falta en ese punto, si en vez de 1 propiedad innecesaria son 15 ya empieza a ser bastante desperdicio, si tengo 1 millon de peticiones estoy mandando 1 millon de categoriasID que no hacen falta.

Flashk

Eso no es HATEOAS.

Y por lo general, aunque con microservicios tengas distintos endpoints para consultar distintas cosas, los endpoints suelen sacar a menudo algunos datos relativos a otros recursos, como por ejemplo, una descripción o un nombre y no solo el id.

Es decir, que si tienes los siguientes endpoints:

  • Listado de vídeos: /videos
  • Detalle de un vídeo: /videos/1234
  • Listado de categorías: /categories
  • Detalle de una categoría: /categories/1234
  • Listado de autores: /authors
  • Detalle de un autor: /authors/1234

Si consultas un vídeo concreto , lo ideal es traerte al menos el nombre de la categoría y el autor:

GET /videos/1234
{
	id: 1,
	title: "Le corto el pelo a mi novia",
	category: {
		id: 23.
		description: "extreme sports"
	},
	autor: {
		id: 456.
		name: "wuaixd"
	}
}

Luego ya, si quieres obtener detalles más concreto de una categoría o de un autor, entonces si, te diriges al endpoint concreto, por ejemplo, al consultar la categoría, te podrían venir datos como cuantos vídeos hay en esa categoría y que popularidad tiene:

GET /categories/23
{ 
	id:23,
	description: "extreme sports",
	videoCount: 2804,
	popularity: 7
}

Con microservicios no se trata de forzar al cliente a hacer un millón de peticiones para poder pintar un listado básico, sino que si tienes varios componentes que pintar, cada uno sea un servicio (puede que alguno más si es algo más complejo, pero de manera justificada), de tal manera que si el backend de uno de ellos está caído, el resto se pueda seguir pintando.

Por ejemplo, imagina que tu aplicación pinta tras hacer una búsqueda de un vídeo:

  • Un listado con los vídeos encontrados.
  • Otro listado pero con los vídeos más populares del momento (independientemente de lo que hayas buscado).
  • Un listado de amigos.
  • Un listado de subscripciones mostrándote que subscripciones tienen novedades.

Pues cada una de estas cosas podría ser un servicio independiente:
GET /videos
GET /trending-videos
GET /friends
GET /subscriptions

Esta granularidad, para empezar está bien. Demasiada granularidad significa hacer que un determinado componente web tenga que esperar a que todas sus peticiones terminen.

Es decir, como bien dices tú, si al consultar un listado de 20 vídeos, para cada uno de ellos tengo que consultar su categoría, autor y demás, entonces el componente web de vídeos y el de trending-vídeos tendrían que esperar bastante:
GET /videos

Y a continuación para cada vídeo del listado:
GET /videos/1 (categoría 2, autor 123)
GET /categories/2
GET /authors/123

Efectivamente resultado en 20*3 = 60 peticiones + 1 petición del listado principal 61, y como en mi ejemplo te he añadido además un endpoint de trending-videos, se duplicarían aun más las peticiones.

Diles a los de tu backend que echen un ojo a APIs como la de Spotify. La API de Spotify tiene cierto nivel de HATEOAS, y aún con esas, endpoints como el de obtener un canción, sacan, por ejemplo, el artista/album de la canción y no solo lo que es la uri para consultar el detalle, sino cierta cantidad de datos básicos, para que no se hagan demasiadas peticiones.

2 1 respuesta
JuAn4k4

Backend for frontend gateway.

Esos endpoints están muy bien para cachear y tal...

2 1 respuesta
r2d2rigo

El API deberia permitirte hacer query de varios objetos al mismo tiempo mediante ID y devolverte una lista, si no el iluminado que la ha dise;ado es un mierdas que solo se leyo un hello world de REST.

1 respuesta
TitoBurns

#12
Pero como vas hacer una query de varios objetos por ID si #1 necesita usar la llamada GET /videos para poder printar el listado de videos? :thinking:

Yo lo veo como dice #3, me crearía un servicio para que te devuelva lo que tu necesitas pero lo que deberían hacer los de back es lo que dice #10. Que modifiquen el endpoint para que devuelva mas información de la categoría y el autor...es que vamos estas pidiendo la información más básica de esas entidades, no tiene ningún sentido tener que ir haciendo peticiones para cada uno

2 respuestas
r2d2rigo

#13

GET /videos

[
{
videoId: 1;
titulo: "Le corto el pelo a mi novia";
categoriaId: 3;
autorId: 13213;
},
{
videoId: 2;
titulo: "El fontanero y la mujer";
categoriaId: 5;
autorId: 1337;
}
]

GET /autores?id=13213,1337

[
{
autorId: 13213;
nombre: "Pepito Perez"
},
{
autorId: 1337;
nombre: "Desu"
}
]

Lo otro que se me ocurre es que si un objeto referencia a otro, en vez de devolver la ID del referenciado (categoria, autor) embeba en el objeto una representacion parcial que contenta suficiente informacion para la mayoria de queries. Luego si necesitas obtener toda la info de una categoria/autor pues si, GET separados.

EDIT: habia hecho skip de varios mensajes y mi segunda opcion es como dice #11, lo recomiendo bastante.

2 1 respuesta
TitoBurns

Vale, no te había entendido, la primera solución me parece ingeniosa (la de hacer por IDs separado con comas) pero me sigue gustando más la segunda, la de devolver ya la info necesaria en el primer GET y luego si se necesita más info detallada pues GET por separado.

No acabo de ver que casos podria ser mejor la primera, porque si se devuelve algo más de info y en millones de llamadas podría relantizar el backend pero es que todo eso lo pierdes en el front teniendo que hacer mas peticiones

Flashk

#13 Tienes el GET /videos para sacar el listado, y el GET /videos/1234 para sacar el detalle de un vídeo concreto.

Si necesitas sacar múltiples videos, tienes el listado, si necesitas sacar uno en concreto, tienes el detalle.

¿En qué situaciones se necesita buscar por múltiples ids a la vez? Cuando se dan esos casos, suele ser por un subconjunto de datos que guardan algo en común, por lo que quizá puedas reutilizar un servicio existente o tal vez sea necesario plantear el uso de subrecursos.

Por ejemplo, si quiero buscar los vídeos de un autor concreto, podría inicialmente plantearse así:

GET /authors/1234

{
	"id": 1234
	"videos": [
		1, 32, 56 
	]
}

Y luego buscar por identificadores:

GET /videos?id=1,32,56

Pero en realidad estamos tratando con un subconjunto de datos, que son los vídeos de un determinado autor, así que, ¿por qué no filtrar por dicho autor en el listado, o añadir un subrecurso al endpoint de autores?

Por lo que se podría hacer esto otro:

/videos?authorId=1234 

O bien con subrecursos:

/authors/1234/videos

Y luego, lo que dice #14 de embeber una representación parcial, es lo ideal (es lo que hace spotify). Siendo así, probablemente no tengas que hacer búsquedas de detalle, las búsquedas de detalle son ya para cuando seleccionas un item del listado y abres en tu aplicación para ver todos sus datos.

Lan0s

Imaginaos la experiencia del usuario en una app que tire de esta API, por ejemplo, al consultar el feed de vídeos, que probablemente sea la vista principal de la misma.

Muchas veces nos centramos tanto en detalles de implementación (que si es REST pura o no, si hago esto así o allá, si sigue esta tendencia o la otra) que olvidamos el motivo por el que se crea el software.

1 respuesta
JuAn4k4

#17 Si tienes todo metido en el mismo sacó por película, imagínate que luego quieres buscar un actor y todas sus películas.

TitoBurns

Otra solución que se me ocurre (nunca la he implementado) seria poder pasarle parámetros opcionales a /videos especificando los campos que quieres que te devuelva toda la información de la entidad linkada.

Es decir si videos dices que te devuelve esto:

[
{
videoId: 1;
titulo: "Le corto el pelo a mi novia";
categoriaId: 3;
autorId: 13213;
},
{
...
}
]

Pues poder llamar a /videos?cataegoriaExt&autorExt y que te devolviera esto:

[
{
videoId: 1;
titulo: "Le corto el pelo a mi novia";
categoria: 
  {
    categoriaId: 1,
    nombre: "suspense",
    descripción: "que emoción"
    ...
}
autor: 
  {
    autorId: 5,
    nombre: "pepe",
    descripción: "el malote"
    ...
}
},
{
...
}
]

Obviamente a este endpoint se le tendría que poner paginación para que no saturara la bbdd pero puede ser una solución (no tengo ni idea si existe un nombre para esta estrategia)
Si alguien ha implementado algo así tengo curiosidad para saber que opinión tiene :)

1 respuesta
desu

No tienen ni puta idea. Yo tambien lo he sufrido. En lugar de cargar una vista entera que eran 5kb y 1 request hacian que el usuario tuviese que ir haciendo clicks.. al final diez veces mas request y tardabas diez veces mas porque si... XD Yo hago endpoints específicos para casi todo, que suele ser web y mobil que tambien desarrollo yo... y adew.

No tengo nada que a;adir a lo que ya te han dicho los expertos.

Tan solo quiero decir que el mayor problema no es el dise;o, el problema es que tengas un equipo y algun "tech lead" o similar que justifique esa mierda y no sepa que eso esta mal. Red flag.

Quedate con el patron de dise;o que te comentan porque es el bueno y en la proxima entrevista de trabajo ya sabes que tema sacar.

pantocreitor

#19 algo del estilo lo hice yo hace un par de meses para un modal de selección que según desde donde lo llamases necesitabas unos datos u otros de otras tablas con las que se relacionaba.
Le pasaba un json/entidad opciones con las entidades que me quería traer a true.
Igual que hice esto podría haber hecho 3 endpoints o 4 (no recuerdo exactamente cuantas posibilidades había) y haber llamado a uno u a otro según lo que necesitase y la verdad que no se cual de las 2 opciones es la más óptima, aunque la que hice no le desagrado al que me encargó la tarea.

1
Kr4n3oK

A mi personalmente me parece una aberración solucionar este problema de esta forma, por mucho REST que sea y su madre. Si no te queda mas remedio, te haces un endpoint en back donde mapee lo que necesites.

¿Supongo que esto vendrá así porque es una base de datos no relacional?

wuaixd

Creo que en mi caso lo más fácil va a ser hacerme endpoints especiales para cada página o bloque.

Si eso no cuela intentaré adaptar los endpoints para que en lugar de recibir ids reciban arrays de ids pero esto ya me jode porque implica un poco de gestión para ver que ids tengo que pedir y para asignarlos cuando lo recibo.

1 respuesta
Ranthas

#23 Eso huele a dos cosas: la primera que la descripción tiene valores null en algunos vídeos y la segunda es que usan alguna librería / framework para mapear automáticamente las entities a los dto que es la que está excluyendo los atributos nulos.

Realmente no te afecta a la hora de mapear la respuesta de la API, en Angular, por ejemplo, si tu clase / interfaz tiene un atributo que no tenga la respuesta de la API, te lo deja a null.

7 días después
D10X

Un tipico caso de CRUD con el parámetro ?detail=1

21 días después
modena

Y si usas GraphQL en lugar de REST? El problema de REST (aun usando un ORM) es que si quieres devolver X campos en una peticion y X+1 en otra al mismo endpoint, al final, o acabas picandote un algoritmo de busqueda a través de queryparams como comentan arriba o acabas separando cada endpoint manualmente para devolver exactamente los datos que necesitas y es un poco tedioso (hay que tener en cuenta en el diseño de APIs que nunca deberías ligar y diseñar el Backend, con cómo tienes que mostrarlo en el front pero bueno esto es algo más a niveles de arquitecturas grandes).

Con GraphQL desde el Angular pides lo que necesitas en cada petición y solo se devuelve lo que pides. Para peticiones que necesitan información de los elementos hijos para mi es perfecto y es super fácil de utilizar.

Espero que te sirva de ayuda.

2 1 respuesta
24 días después
wuaixd

#26 Eso por ahora está descartado pero pienso igual, es lo más flexible y lo que soluciona mucho esto.

Muchas compañias grandes se han pasado o estan pasando a GraphQL y los beneficios que da para mi compensan mucho el coste de meterlo

1

Usuarios habituales

  • wuaixd
  • Ranthas
  • pantocreitor
  • TitoBurns
  • JuAn4k4
  • Flashk
  • r2d2rigo