Recientemente hablé un poco de REST, la manera de diseñar la arquitectura de una API en el backend de una aplicación. Algo que tiene mucho sentido ser muy usado porque maneja muy bien la información de negocio.

Ahora quise hacer un ejemplo real de como usar el verbo GET para obtener información necesaria para una aplicación real.

En este ejemplo básico intentaremos crear un sitio web con Angular en su versión 13, con el objetivo de mostrar platillos de comida con diferentes características, entre ellas, nombre del platillo, ingredientes, las instrucciones de preparación y una imagen representativa.

Si prefieres observar el código para antes de ver explicación puedes verlo aquí.

Obvio necesitamos un backend que distribuya los recursos para poderlos consultar, en este caso ocuparemos un backend ya creado llamado theMealDB, una base de datos de comidas de diferentes regiones, que expone una API pública para el desarrollo de este ejercicio.

El service provider

Parte de mi gusto por Angular es poder utilizar service providers, que básicamente es un conjunto de funciones abstraídas como cajas negras, para ejecutar ciertas acciones necesarias para la aplicación, por supuesto reutilizables. Este es el service provider que utilizaremos llamado meal.service.ts.

Cuenta con los siguientes servicios que utilizará la aplicación ya funcional, obtener un platillo al azar:

 async getMealRandom(): Promise<Meal> {
   const data = await this.fetch(
     `https://www.themealdb.com/api/json/v1/1/random.php`
   );
 
   return data[0];
 }

Obtener un platillo por un identificador:
 async getMeal(id: any = ''): Promise<Meal> {
   const data = await this.fetch(
     `https://www.themealdb.com/api/json/v1/1/lookup.php?i=${id}`
   );
 
   return data[0];
 }

Obtener un listado de platillos, por una búsqueda:

 async getMeals(search: string = ''): Promise<Meal[]> {
   const data = await this.fetch(
     `https://www.themealdb.com/api/json/v1/1/search.php?s=${search}`
   );
 
   return data;
 }

Dado que la API de meals hay una estructura que no me gusta del todo, en donde los ingredientes estan listados así:

Ingredientes lista

Tome la decisión de pasar a un array de elementos cada ingrediente, para después solo iterar ese elemento en un clave, valor. Esto lo haría una función que se encarga de realizar está tarea. Por cierto es un método privado y solo el service provider puede utilizar esta función.

 private formatIngredients(meals: any): Meal[] {
   return meals.map((meal: any) => {
     const ingredientAndQuantity = [];
     for (let index = 1; index <= 20; index++) {
       const ingredientKey = `strIngredient${index}`;
       const measureKey = `strMeasure${index}`;
 
       if (
         meal.hasOwnProperty(measureKey) &&
         meal[measureKey] &&
         typeof meal[measureKey] === 'string' &&
         meal.hasOwnProperty(ingredientKey) &&
         meal[ingredientKey] &&
         typeof meal[ingredientKey] === 'string'
       ) {
         ingredientAndQuantity.push(
           `${meal[ingredientKey].trim()} - ${meal[measureKey.trim()]}`
         );
       }
     }
     return {
       ...meal,
       ingredientAndQuantity,
     };
   });
 }

Como algo interesante, en mi experiencia es común que las APIs tengan un diseño no siempre agradable para el desarrollador frontend, me ha tocado mucho que tenga que procesar información para formatear y mostrar de manera más sencilla ciertos datos, algo que se vuelve complicado cuando los equipos frontend+backend no trabajan de la mano y cada quien hace lo que venga en gana.

También tenemos el método que hace la magia, un fetch que obtiene la información por medio de una petición HTTP GET. Igual de visibilidad privada, algo importante en POO, si una clase desea mantener sus recursos para sí misma.

private fetch(url: string): Promise<Meal[]> {
   return new Promise<Meal[]>((resolve, reject) => {
     try {
       fetch(url)
         .then((response) => response.json())
         .then((data) => {
           const meals = data?.meals || [];
           resolve(this.formatIngredients(meals));
         });
     } catch (error) {
       reject(error);
     }
   });
 }

Una cosa que me gusta en Typescript, que realmente es mejor decir de la programación orientada a objetos, es que podemos declarar interfaces para modelar la información a nuestro diseño. Si se observa definimos una salida en los métodos anteriores, por ejemplo:

 private formatIngredients(meals: any): Meal[]

Lo cual indica que formatIngredients tendrá que arrojar un Array de meals esto viene de nuestro modelo llamado meal.ts:
export interface Meal {
 dateModified: string;
 idMeal: string;
 strArea: string;
 strCategory: string;
 strCreativeCommonsConfirmed: string;
 strDrinkAlternate: string;
 strImageSource: string;
 ingredientAndQuantity: Array<string>;
 strInstructions: string;
 strMeal: string;
 strMealThumb: string;
 strSource: string;
 strTags: string;
 strYoutube: string;
}

Routing

Contamos con dos rutas principales: el home y el detalle del platillo.

const routes: Routes = [
 { path: '', component: HomeComponent },
 { path: 'meal/:mealId', component: MealComponent },
];

Para el home la clase es la siguiente:

export class HomeComponent implements OnInit {
 meals: Meal[] = [];
 constructor(public mealService: MealService) {}
 
 ngOnInit(): void {
   (async () => {
     this.meals = await this.mealService.getMeals();
   })();
 }
}

Se necesita uno de los hooks del ciclo de vida de un componente de Angular llamado ngOnInit, para utilizar la función getMeals() del proveedor de servicios. Con esto obtenemos todos los meals en una variable llamada meals.

Al final solo quedará mostrarlos:

<div class="container mx-auto">
 <app-card *ngFor="let meal of meals" [meal]="meal"></app-card>
</div>

Aquí se redujo parte de la funcionalidad a un componente llamado app-card que contiene lo siguiente en su clase:
export class CardComponent {
 @Input() meal: Meal;
 
 constructor() {}
 
 slice(str: string | undefined) {
   if (!str) return '';
 
   return str.slice(0, 120);
 }
}

Donde la entrada de datos tenemos cada meal o platillo que envía el componente home, el método slice, solo acorta la descripción a 120 caracteres para mostrar contenido no tan extenso en el html:

<div class="p-10 flex justify-center">
 <a [routerLink]="['/meal', meal.idMeal]">
   <div class=" w-full lg:max-w-full lg:flex">
     <div
       class="h-48 lg:h-auto lg:w-48 flex-none bg-cover rounded-t lg:rounded-t-none lg:rounded-l text-center overflow-hidden"
       [style.background-image]="'url('+meal?.strMealThumb+')'" title="Mountain">
     </div>
     <div
       class="border-r border-b border-l border-gray-400 lg:border-l-0 lg:border-t lg:border-gray-400 bg-white rounded-b lg:rounded-b-none lg:rounded-r p-4 flex flex-col justify-between leading-normal">
       <div class="mb-8">
         <p class="text-sm text-gray-600 flex items-center">
         <div class="flex items-baseline">
           <span
             class="bg-teal-200 text-teal-800 text-xs px-2 inline-block rounded-full uppercase font-semibold tracking-wide">
             {{ meal?.strCategory }}
           </span>
           <div class="ml-2 text-gray-600 uppercase text-xs font-semibold tracking-wider">
             {{ meal?.strTags }}
           </div>
         </div>
         <div class="text-gray-900 font-bold text-xl mb-2">{{ meal?.strMeal }}</div>
         <p class="text-gray-700 text-base">{{ slice(meal?.strInstructions) }}
         </p>
         <p class="mt-2">
           <button type="button"
             class="inline-block px-6 py-2.5 bg-blue-800 text-white font-medium text-xs leading-tight uppercase rounded shadow-lg hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out">Read
             more</button>
         </p>
       </div>
       <div class="flex items-center">
 
         <div class="text-sm">
           <span class="text-teal-600 text-md font-semibold">{{ meal?.strArea }}
           </span>
         </div>
       </div>
     </div>
   </div>
 </a>
</div>

La función que realiza este componente es ir mostrando los detalles de cada platillo con la interpolación de Angular, que por cierto se usa tailwind para estilizar y verse más lindo, se coloca la ruta para ver la info completa que es la siguiente página del router.

<a [routerLink]="['/meal', meal.idMeal]">

El resultado final será el siguiente:

platillos home

El detalle del platillo

La clase que se va a manejar para la página de detalle:

export class MealComponent implements OnInit {
 meal: Meal;
 constructor(public mealService: MealService, public route: ActivatedRoute) {}
 
 ngOnInit(): void {
   const routeParams = this.route.snapshot.paramMap;
 
   (async () => {
     this.meal = await this.mealService.getMeal(routeParams.get('mealId'));
   })();
 }
}

Aquí declaramos una variable que va a contener el detalle completo del platillo, inyectamos las dependencias en el constructor entre ellas el service provider que generamos y un gestor de rutas. Que justamente en la url de la aplicación cuenta con el identificar que envia el ruoterLink como lo vemos aquí:

<a [routerLink]="['/meal', meal.idMeal]">

Con esto, en el hook ngOnInit podemos obtener ese parámetro de la url para enviarlo al service provider, específicamente al método getMeal().

El html para mostrar el detalle del platillo es el siguiente:

<div class="container mx-auto">
   <div class="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-1 gap-6">
 
       <div class="flex justify-center p-6" *ngIf="meal && meal.idMeal">
           <div class="relative px-4">
               <div class="bg-white p-6 rounded-lg shadow-lg">
                   <div class="flex items-baseline">
                       <span
                           class="bg-teal-200 text-teal-800 text-xs px-2 inline-block rounded-full uppercase font-semibold tracking-wide">
                           {{ meal?.strCategory }}
                       </span>
                       <div class="ml-2 text-gray-600 uppercase text-xs font-semibold tracking-wider">
                           {{ meal?.strTags }}
                       </div>
                   </div>
                  
 
                   <h4 class="mt-1 text-xl font-semibold uppercase leading-tight truncate">
                       {{ meal?.strMeal }}
                   </h4>
 
                   <div class="mt-4">
                       <span class="text-teal-600 text-md font-semibold">Ingredients
                       </span>
                   </div>
 
                   <div class="mt-4 ml-3">
                       <ul class="list-disc">
                           <li *ngFor="let ingredient of meal?.ingredientAndQuantity">{{ingredient}}</li>
                       </ul>
                   </div>
                  
                   <div class="mt-4">
                       <span class="text-teal-600 text-md font-semibold">Instruccions
                       </span>
                   </div>
 
                   <div class="mt-1">{{ meal?.strInstructions }}
                       <span class="text-gray-600 text-sm">
                       </span>
                   </div>
              
                   <div class="mt-4">
                       <button type="button"
                           routerLink=""
                           class="inline-block px-6 py-2.5 bg-blue-800 text-white font-medium text-xs leading-tight uppercase rounded shadow-lg hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out">Back</button>
                   </div>
               </div>
           </div>
           <img [src]="meal?.strMealThumb" alt=" random imgee"
               class="w-full object-cover object-center rounded-lg shadow-md" />
       </div>
   </div>
</div>
 

El resultado:

detalle platillo

Con eso finalizamos la implementación de los métodos GET utilizados en las páginas principales de este ejemplo básico, notarás que toda la información es obtenida de forma dinámica al navegar en la aplicación.

Como un plus al iniciar la aplicación se lanza un modal que muestra un platillo random, digamos una sugerencia de plato del día o algo por estilo.

Ese componente está incrustado en el home de la aplicación:

<app-modal class="visible transition-all duration-300 ease-in animate-spin"></app-modal>

La clase es la siguiente:

export class ModalComponent implements OnInit {
 meal: Meal;
 
 constructor(private el: ElementRef, public mealService: MealService) {}
 ngOnInit() {
   (async () => {
     this.meal = await this.mealService.getMealRandom();
   })();
 
   this.el.nativeElement.addEventListener('click', () => {
     this.close();
   });
 }
 close() {
   this.el.nativeElement.classList.remove('visible');
   this.el.nativeElement.classList.add('invisible');
 
   console.log('leave');
 }
}
  

Al hook ngOnInit Obtenemos de nuestro service provider la función de getMealRandom y añadimos una referencia al HTML de este componente, a esta referencia le colocamos al evento click que ejecute una función, la llamada close(), que básicamente añade una clase invisible y remueve la clase visible que se encuentran en tailwind.

El HTML es el siguiente:

<div class="Modal">
   <h1 class="text-center text-2xl text-slate-50">⭐ Meal Favorite ⭐</h1>
   <app-card [meal]="meal"></app-card>
</div>
<div class="Modal-overlay"></div>

Y este resultado veremos al ir al home:

random platillo

Algo interesante me parece crear un modal sencillo sin necesidad de instalar mil librerías.

Este ejemplo lo podrás probar ejecutando en la raíz del proyecto:

docker-compose up

Al terminar de levantar el servicio de docker se podrá consultar en http://localhost:4201/.

¡Saludos!