Apis Services en Nuxt 3 con TypeScript
En este apartado vamos a enseñar cómo manejar las llamadas a servicios API en una aplicación Nuxt 3 con TypeScript (utilizando el sistema actual).
En primera instancia, vamos a ver y entender el código base con el que se trabajan estas llamadas
Código Base
import type { FormatData } from "@/types/zoho";import { API } from "@/constants/apis";
const STATUS = { error: "error", success: "success", idle: "idle", pending: "pending",};class HttpClient { private getUrl() { return API; }
private formatParams(params: { name: string; value: string }[]) { return params.length === 0 ? "populate=*" : params.map((param) => `${param.name}=${param.value}`).join("&"); }
private static handleErrorResponse<T>(error: boolean, response: T[]) { if (response.length === 0 && error) { throw createError({ statusCode: 404, statusMessage: "Page Not Found", fatal: true, }); } }
private convertToURLParams(params: FormatData) { const formData = new URLSearchParams(); for (const key in params) { formData.append(key, String(params[key])); } return formData; }
async get<T>( resource: string, params: { name: string; value: string }[] = [], shouldThrowError: boolean = true ) { try { if (!resource) throw new Error("Resource is not provided");
const formatParams = this.formatParams(params); const { data, status, error, pending } = await useFetch( `${this.getUrl()}api/${resource}?${formatParams}`, { onResponse({ response: { _data: { data: response }, }, }) { HttpClient.handleErrorResponse(shouldThrowError, response); }, } ); const { data: response } = data.value as Record<"data", T>; if (status.value === STATUS.error && error.value) { throw new Error(error.value.message); } return { response, status, error, pending }; } catch (error) { throw createError({ statusCode: 404, statusMessage: "Page Not Found", fatal: true, }); } }
public async post( resource: string, body: Record<string, any>, params: { name: string; value: string }[] = [] ) { try { if (!resource) throw new Error("Resource is not provided"); if (!body) throw new Error("Body is not provided");
const formatParams = this.formatParams(params);
const { data: { value: response }, status: { value: status }, error, pending, } = await useFetch(`${this.getUrl()}api/${resource}?${formatParams}`, { method: "POST", body: JSON.stringify(body), });
if (status === STATUS.error && error.value) throw new Error(error.value.message);
return { response, status, error, pending }; } catch (error) { console.error(error); throw error; } }
public async postZoho(resource: string, params: FormatData) { const body = this.convertToURLParams(params);
const { data: { value: response }, status: { value: status }, error, pending, } = await useFetch(`https://flow.zoho.com/${resource}`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body, });
return { response, status, error, pending }; }
public async getWithoutDestructure<T>( resource: string, params: { name: string; value: string }[] = [], shouldThrowError: boolean = true, headers = {} ) { try { if (!resource) throw new Error("Resource is not provided");
const formatParams = this.formatParams(params);
const { data, status, error, pending } = await useFetch( `${this.getUrl()}api/${resource}?${formatParams}`, { headers, onResponse({ response: { _data: response } }) { HttpClient.handleErrorResponse(shouldThrowError, response); }, } ); const response = data.value as T; if (status.value === STATUS.error && error.value) { throw new Error(error.value.message); } return { response, status, error, pending }; } catch (error) { throw createError({ statusCode: 404, statusMessage: "Page Not Found", fatal: true, }); } }}
export const httpClient = new HttpClient();Para empezar, vamos a ver las importaciones que tenemos en lo más alto del archivo.
FormatData
export type FormatData = Record<string, string | number | boolean>;API
Aquí se define la URL de la API que se va a utilizar, dependiendo del entorno en el que se encuentre la aplicación.
export const API = process.env.NODE_ENV === ENVIRONMENT.development ? LOCAL_URL : PRODUCTION_API_URL;STATUS
Se define un objeto STATUS con los diferentes estados que puede tener una solicitud a la API.
const STATUS = { error: "error", success: "success", idle: "idle", pending: "pending",};Clase HttpClient
La clase HttpClient es la encargada de manejar las solicitudes a la API. En ella se definen los métodos getUrl, formatParams,
handleErrorResponse, convertToURLParams, get, post, postZoho y getWithoutDestructurepara realizar
las solicitudes a la API.
Método getUrl
Este se encarga de devolver la URL de la API que se va a utilizar.
private getUrl() { return API}Método formatParams
Este método se encarga de formatear los parámetros que se van a enviar en la solicitud a la API.
private formatParams(params: { name: string; value: string }[]) { return params.length === 0 ? 'populate=*' : params.map((param) => `${param.name}=${param.value}`).join('&')}Método handleErrorResponse
Este método se encarga de manejar los errores que se puedan producir en la solicitud a la API.
private static handleErrorResponse<T>(error: boolean, response: T[]) { if (response.length === 0 && error) { throw createError({ statusCode: 404, statusMessage: 'Page Not Found', fatal: true, }) }}Método convertToURLParams
Este método se encarga de convertir los parámetros a formato de URL. Este método se utiliza en el método postZoho, que como su nombre indica, se utiliza para realizar solicitudes a la API de Zoho.
private convertToURLParams(params: FormatData) { const formData = new URLSearchParams() for (const key in params) { formData.append(key, String(params[key])) } return formData}Método get
Este método se encarga de realizar solicitudes GET a la API. Utilizando el composable useFetch proveniente de Nuxt.
Le pasa la URL de la API, los parámetros, y un callback para manejar la respuesta. En caso de que la solicitud falle, se lanza un error.
Esta última parte viene dada por el método handleErrorResponse.
async get<T>( resource: string, params: { name: string; value: string }[] = [], shouldThrowError: boolean = true,) { try { if (!resource) throw new Error('Resource is not provided')
const formatParams = this.formatParams(params) const { data, status, error, pending } = await useFetch( `${this.getUrl()}api/${resource}?${formatParams}`, { onResponse({ response: { _data: { data: response }, }, }) { HttpClient.handleErrorResponse(shouldThrowError, response) }, }, ) const { data: response } = data.value as Record<'data', T> if (status.value === STATUS.error && error.value) { throw new Error(error.value.message) } return { response, status, error, pending } } catch (error) { throw createError({ statusCode: 404, statusMessage: 'Page Not Found', fatal: true, }) }}Método post
Este método se encarga de realizar solicitudes POST a la API. Utilizando el composable useFetch proveniente de Nuxt.
public async post( resource: string, body: Record<string, any>, params: { name: string; value: string }[] = [], ) { try { if (!resource) throw new Error('Resource is not provided') if (!body) throw new Error('Body is not provided')
const formatParams = this.formatParams(params)
const { data: { value: response }, status: { value: status }, error, pending, } = await useFetch(`${this.getUrl()}api/${resource}?${formatParams}`, { method: 'POST', body: JSON.stringify(body), })
if (status === STATUS.error && error.value) throw new Error(error.value.message)
return { response, status, error, pending } } catch (error) { console.error(error) throw error } }Método postZoho
Este método se encarga de realizar solicitudes POST a la API de Zoho. Utilizando el composable useFetch proveniente de Nuxt.
En este usamos el método convertToURLParams para convertir los parámetros a formato de URL.
public async postZoho(resource: string, params: FormatData) { const body = this.convertToURLParams(params)
const { data: { value: response }, status: { value: status }, error, pending, } = await useFetch(`https://flow.zoho.com/${resource}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body, })
return { response, status, error, pending } }Método getWithoutDestructure
Este método se encarga de realizar solicitudes GET a la API. Utilizando el composable useFetch proveniente de Nuxt.
En este caso, no se deestructura la respuesta, sino que se devuelve tal cual. Esto se hizo, ya que para obtener los
“locales” del Strapi, estos no venían envueltos en un objeto data.
public async getWithoutDestructure<T>( resource: string, params: { name: string; value: string }[] = [], shouldThrowError: boolean = true, headers = {}, ) { try { if (!resource) throw new Error('Resource is not provided')
const formatParams = this.formatParams(params)
const { data, status, error, pending } = await useFetch( `${this.getUrl()}api/${resource}?${formatParams}`, { headers, onResponse({ response: { _data: response } }) { HttpClient.handleErrorResponse(shouldThrowError, response) }, }, ) const response = data.value as T if (status.value === STATUS.error && error.value) { throw new Error(error.value.message) } return { response, status, error, pending } } catch (error) { throw createError({ statusCode: 404, statusMessage: 'Page Not Found', fatal: true }) } }Para terminar, se hace una exportación de la clase HttpClient como una constante para poder utilizarla en otros archivos.
Genrando servicios
A la hora de crear un servicio, necesitamos crear un archivo en la carpeta services que contenga la lógica de la solicitud a la API. Por ejemplo, en este archivo llamado
homeApiService.ts se ha creado la lógica para obtener los datos de la página de inicio.
import { httpClient } from "./useFetchApi";import type { Home } from "@/interfaces/api/home";import type { LanguageCode } from "@/types/locale";import { LOCALE } from "@/constants/languages";
export const homeApiService = async (lang: LanguageCode) => { const params = [ { name: "populate[hero_block][populate][video]", value: "*", }, { name: "populate[partners][populate][tabs][populate]", value: "*", }, { name: "populate[will_fill][populate][cards][populate]", value: "*", }, { name: "populate[pills][populate][vertical_image][populate]", value: "*", }, { name: "populate[our_content][populate]", value: "*", }, { name: "populate[media_carousel][populate][background_media][populate]", value: "*", }, { name: "populate[for_you][populate][icon][populate]", value: "*", }, { name: "populate[seo][populate][metaImage]", value: "*", }, { name: "populate[testimonials][populate]", value: "*", }, { name: LOCALE, value: lang, }, ]; const { response, pending } = await httpClient.get<Home>( "alebat-education-home", params ); return { response, pending };};Importaciones
Pasaremos a ver una lista de las importaciones que se han realizado en el archivo homeApiService.ts y ver que contiene cada una de ellas. Como es obvio, no se mostrará lo que contiene httpClient, ya que
se explica todo anteriormente.
Home
Esto es lo que posee la interfaz de Home. Puedes generar interfaces automáticamente usando Quicktype y pasandole la respuesta de tu servicio.
export interface Card { readonly description: string; readonly icon: Media; readonly id: number; readonly title: string;}
export interface ForYou { readonly description: string; readonly id: number; readonly icon: MediaForYou; readonly title: string;}
export interface HeroBlock { readonly id: number; readonly subtitle: string; readonly title: string; readonly video: Media[];}
export interface Tab { readonly id: number; readonly logotypes: Media[]; readonly tab_title: string;}
export interface OurContent { readonly id: number; readonly description: string; readonly image: Media; readonly title: string; readonly video: Media;}
export interface Partner { readonly id: number; readonly tabs: Tab[]; readonly title: string;}
export interface Pill { readonly description: string; readonly id: number; readonly pills: string[] | null; readonly title: string; readonly vertical_color: string | null; readonly vertical_image: Media; readonly vertical_url: string | null;}
export interface Testimonial { id: number; image: Media; video: Media;}
export interface BackgroundCarousel { readonly id: number; readonly description: string; readonly background_media: Media; readonly link: Media;}
export interface WillFill { readonly cards: Card[]; readonly id: number; readonly title: string;}
export interface Home { readonly createdAt: Date; readonly for_you: ForYou[]; readonly hero_block: HeroBlock; readonly id: number; readonly locale: string; readonly our_content: OurContent[]; readonly partners: Partner; readonly pills: Pill[]; readonly publishedAt: Date; readonly testimonials: Testimonial[]; readonly updatedAt: Date; readonly seo: SEO; readonly media_carousel: BackgroundCarousel[]; readonly will_fill: WillFill;}LanguageCode
Esto es lo que posee el tipo de LanguageCode.
export type LanguageCode = "en" | "es" | "pt-BR";LOCALE
Esto es lo que posee la constante LOCALE.
export const LOCALE = "locale";Lógica de la solicitud
En este archivo se ha creado la lógica para obtener los datos de la página de inicio. Los parámetros que se usan en la solicitud son aquellos que se defienen en el Strapi antes de hacer el servicio.
Se añade el locale y el idioma para poder usar las traducciones, en el caso de que tu servicio no posea traducciones por alguna razón, puedes omitir el parámetro LOCALE en los parámetros.
Como podemos observar en esta línea:
const { response, pending } = await httpClient.get<Home>( "alebat-education-home", params);Hacemos una petición GET a la API con el endpoint alebat-education-home y los parámetros que hemos definido anteriormente. También vemos como se le pasa por genérico el tipo de respuesta que se espera, en este caso
se espera que lo que posea response, sea la interface Home. Pending por otro lado, nos devolvera true o false dependiendo si la solicitud está en proceso o no.
Exportación
Por último, se exporta la función homeApiService para poder utilizarla en otros archivos. Puedes exportar el resto de parámetros que necesites, pero en este caso, solo se exporta response y pending.
return { response, pending };Generando servicio para Zoho
En este caso, se ha creado un archivo llamado zohoApiService.ts para manejar las solicitudes a la API de Zoho.
import { httpClient } from "./useFetchApi";import { contactResource } from "@/constants/zoho";import type { ContactZoho, FormatData } from "@/types/zoho";
export const requestContact = async (data: ContactZoho) => { const formattedData: FormatData = { ...data, }; const response = await httpClient.postZoho(contactResource, formattedData); return response;};Importaciones
Pasaremos a ver una lista de las importaciones que se han realizado en el archivo zohoApiService.ts y ver que contiene cada una de ellas. Como es obvio, no se mostrará lo que contiene httpClient, ya que
se explica todo anteriormente.
contactResource
Este unicamente contiene el endpoint de la API de Zoho. No incluimos la URL base, ya que esta se encuentra en el archivo httpClient.ts.
Hacemos incapié en que esta cadena no posee el https://flow.zoho.com/ ya que se encuentra en el archivo httpClient.ts.
export const contactResource = "693742817/flow/webhook/incoming?zapikey=1001.e4ff16d065dcad3badf30f55e964666e.2ee548f30ca66e2e0c81068d0862d7fa&isdebug=false";ContactZoho
Esto es lo que posee la interfaz de ContactZoho. Esta interfaz indica los datos que recibe la API de Zoho. Los cuales son los indicados en el formulario de contacto.
export interface ContactZoho { name: string; email: string; country: string; code: string; phone: string; subject: string; policy: boolean;}FormatData
Esto es lo que posee la interfaz de FormatData. Esta interfaz indica los datos que recibe la API de Zoho. Los cuales son los indicados en el formulario de contacto. Puedes darte cuenta, que tiene valores diferentes a la interfaz ContactZoho, esto se debe a que la API de Zoho espera los datos en un formato diferente. ContactZoho indicaria el tipo de datos que llegan por parámetro, esto para asegurarnos que siempre que se use, llegarán estos datos, y FormatData, se utiliza para darle formato a los datos que se enviarán a la API.
export type FormatData = Record<string, string | number | boolean>;Lógica de la solicitud
En primera instancia, usamos una constante, en este caso llamada formattedData, que utilizamos para cambiar el parámetro data a un formato que la API de Zoho espera.
const formattedData: FormatData = { ...data,};En la siguiente línea, hacemos una petición POST a la API de Zoho con el endpoint contactResource y los datos formateados que hemos definido anteriormente.
const response = await httpClient.postZoho(contactResource, formattedData);Exportación
Por último, se exporta la función requestContact para poder utilizarla en otros archivos. En este caso, solo se exporta response.
return response;Composable para los Servicios API
Para manejar las llamadas a los servicios API, vamos a utilizar un composable que nos permitirá hacer las solicitudes GET a la API. Este composable se encargará de hacer la solicitud y devolver la respuesta.
Código del Composable
Este código se encuentra en el archivo useServices.ts en la carpeta composables. A este composable se le porporciona un type genérico T que se utilizará para definir el tipo de respuesta que se espera del servicio API.
Por otro lado, se le pasa un callback, que es uno de los servicios que tengamos creados anteriormente. Este callback recibe el idioma y devuelve la respuesta del servicio en el idioma que nos encontremos actualmente.
import type { LanguageCode } from "@/types/locale";
export async function useServices<T>( callback: (lang: LanguageCode) => Promise<{ response: T; }>) { const { locale } = useI18n(); const data = await callback(locale.value as LanguageCode).then( (res) => res.response ); return { data };}Vamos a proceder al uso de todo lo visto anteriormente
Paso 1: Llamamos a la api en el navegador / postman / rapidapi
Cuando vayamos a crear un servicio, necesitamos saber a que endpoint nos queremos dirijir, que parámetros necesitamos, y que tipo de solicitud vamos a hacer, etc… Utilizamos estas herramientas (navegador, postman, rapidapi, etc…) para poder hacer pruebas y ver que todo está funcionando correctamente. Con los datos que nos devuelve, podemos pasar al siguiente pasos
Paso 2: Definir las Interfaces
Para manejar la estructura de los datos que recibimos de la API, vamos a definir unas interfaces en TypeScript. Esto nos ayudará a tener un código más claro y con mejor mantenimiento. Podemos usar para esto Quicktype, que nos ayudará a generar las interfaces automáticamente.
Paso 3: Crear el servicio
A la hora de crear un servicio, necesitamos crear un archivo en la carpeta services que contenga la lógica de la solicitud a la API. Por ejemplo, en este archivo
llamado homeApiService.ts se ha creado la lógica para obtener los datos de la página de inicio. En este archivo se ha creado la lógica para obtener los datos de la página de inicio.
Podemos usar el ejemplo utilizado anteriormente para saber como debemos crear el servicio.
Paso 4: Uso del composable
A continuación, vamos a ver cómo utilizar el composable useServices en una página de Nuxt para hacer una llamada a un servicio API. En este caso, se ha utilizado el servicio homeApiService.
En este ejemplo, Data es la interfaz que hemos definido anteriormente para manejar la estructura de los datos que recibimos de la API.
const response = ref<Data>();onBeforeMount(async () => { response.value = await useServices<Data>(homeApiService);});Paso 5: Mostrar los datos en la página
Una vez que hemos obtenido los datos de la API, podemos mostrarlos en la página. En este caso, se ha utilizado la variable response para mostrar los datos en la página.
<template> <div> <h1>{{ response.title }}</h1> <p>{{ response.description }}</p> </div></template>Paso 6: Uso de props
Para utilizar los datos en un componente, podemos utilizar las props. En este caso, se ha utilizado el tipo WillFill para definir las props en el script setup.
<script setup lang="ts">import type { WillFill } from "@/interfaces/api/home";defineProps<WillFill>();</script><template> <section class="will-fill"> {{$props.title"}} </section></template>