DEV Community

Cover image for REACT - Consultas a API [Completo]
Chris
Chris

Posted on • Edited on

REACT - Consultas a API [Completo]

Uno de los factores más utilizados a la hora de desarrollar un proyecto en React, es la comunicación con API’s, ya sea a nuestro respectivo Back End o a requisitos externos.

Nos enfocaremos en cómo, y en dónde hacer estas consultas. Como manejar las respuestas, los errores, como integrar TypeScript para hacer explícita tanto la respuesta como los parámetros de las funciones y cómo crear un Custom Hook básico para manejo de mensajes.

Tocaremos también el cómo autenticar las consultas a través de token en Header, como crear una ruta para envío de archivos y como hacer que nuestra ruta identifique si estamos en localhost o producción. Esto es especialmente útil para mantener un desarrollo fluido y consistente. Nos mantendremos fuera del Back End en este caso, solo nos enfocaremos en nuestro proyecto React, vamos a suponer que ya tenemos nuestra API creada y configurada.

Nuestro ejemplo será una pequeña app que consulta, crea, modifica y elimina órdenes.

Estos son los puntos que tocaremos:


Custom Hook

Suponiendo que ya tenemos un proyecto creado y configurado (sino, utilizaremos Vite para crear nuestro proyecto), ¿Cómo hacemos consultas a una API?

1. Crear el Custom Hook.

En este ejemplo llamaremos al archivo ‘fetch.tsx’, ‘.tsx’ adelantándonos a la configuración de Typescript y lo colocamos en el siguiente directorio:

- src
   - Hooks
      - fetch.tsx
Enter fullscreen mode Exit fullscreen mode

2. Inicializar el archivo

export function fetchLibrary () {

  return {}
}
Enter fullscreen mode Exit fullscreen mode

Esto nos inicializa el hook, ahora podemos empezar a crear los request.


Consulta GET

Ahora crearemos la primera consulta, en el momento más básico se vería así:

interface ApiGetResponse {
    data: any[]; // Puedes especificar un tipo más específico para los elementos de data
    status: number; // Puedes especificar un tipo más específico para los elementos de status
  }

  const fetchApiGet = async ({ url } : { url: string }): Promise<ApiGetResponse> => {
    const headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    };

    const request = await fetch(url, {
      headers,
      method: 'GET'
    });

    if (!request.ok) {
      throw new Error('Network response was not ok');
    }

    const response: ApiGetResponse = await request.json();

    return response;
  };
Enter fullscreen mode Exit fullscreen mode

Esto nos genera una consulta GET básica con una respuesta suponiendo que viene un objeto ‘data’ con un Array y un item ‘status’, esto depende de cómo esté estandarizada nuestra API.

La función recibe un único parámetro ‘url’ que es de tipo string, se declaran los headers de forma estándar y se devuelve la respuesta.

Así nos quedaría el hook por ahora:

interface ApiGetResponse {
    data: any[];
    status: number;
  }
export function fetchLibrary () {

  const fetchApiGet = async ({ url } : { url: string }): Promise<ApiGetResponse> => {
  }


  return {fetchApiGet}
}
Enter fullscreen mode Exit fullscreen mode

En este momento ya podemos llamar al Custom Hook y realizar consultas, pero antes de eso, primero haremos otro Custom Hook específico que manejará las consultas de ´Orders´.

En este artículo solo utilizaremos órdenes como ejemplo, así que tendremos un solo Custom Hook, pero supongamos que ustedes hacen consultas de ´User´, ´Products´, ´Discounts´, etc, por cada tipo de modelo, haremos un Custom Hook propio.

- Hooks
   - Models
      - orders.tsx
Enter fullscreen mode Exit fullscreen mode

Acá importamos la librería que acabamos de crear e inicializamos nuestro primer request:

import { fetchLibrary } from '../fetch'

export function useOrders () {
  const { fetchApiGet } = fetchLibrary()

  const ListOrders = async () => {
    try {
      const listOrders = await fetchApiGet({
        url: 'https://nuestra-api/orders'
      })

      if (listOrders instanceof Error || listOrders.status !== 200) {
        throw new Error('Success false')
      }

      return listOrders
    } catch (e) {
      throw new Error('Get Orders failed')
    }
  }

  return { ListOrders }
Enter fullscreen mode Exit fullscreen mode

Con esto estandarizamos la consulta y respuesta para la obtención de Orders, así mantenemos independencia en cada uno de los pasos.

Este Custom Hook se encarga únicamente de consultar las Órdenes y todas las consultas relacionadas con ellas, no le interesa cómo se consultan, quién las hace ni de dónde vienen, esto nos da la libertad de que, si el día de mañana queremos cambiar la librería de consultas de Fetch a Axios, o incluso sacar los datos a través de un archivo JSON, no habría ningún problema. No tendríamos que cambiar cada una de las funciones en donde se consulta ‘Orders’, sino únicamente modificar el Hook fetchLibrary, siempre y cuando se mantenga y se respete el ‘Contrato’ entre todas las partes, todo seguiría funcionando.

Ahora sí podemos realizar la consulta desde nuestra vista o componente de la siguiente manera:

import { useEffect } from 'react'
import { useOrders } from '../../../Hooks/Models/orders'

export default function MyOrders () {
  const { ListOrders } = useOrders()

  useEffect(() => {
    handleGetOrders()
  }, [])

  function handleGetOrders () {
    ListOrders()
      .then((result) => {
        console.log(result)
      })
  }
}
Enter fullscreen mode Exit fullscreen mode

Así podemos manejar de forma sencilla, ordenada y escalable nuestras consultas básicas. Ahora, esto por supuesto se puede mejorar y desarrollar mucho más, hagamos algunas mejoras.


Validar URL para producción

Esta función nos sirve para obtener el dominio de la ruta de forma dinámica, por ejemplo, usar ruta en Producción cuando el proyecto se encuentre en el servidor, o utilizar localhost cuándo nos encontremos trabajando desde nuestro entorno.

Es una función simple que llamaremos desde un archivo como ´utils´

1. Creamos el archivo

- src
   - Utils
      - utils.tsx
Enter fullscreen mode Exit fullscreen mode

2. Creamos la función

export function validProductionUrl({ url }: { url: string }): string {
  if (import.meta.env.VITE_APP_ENV === 'production') {
    return `${import.meta.env.VITE_API_URL}${url}`
  } else {
    return `${import.meta.env.VITE_API_URL_LOCAL}${url}`
  }
}
Enter fullscreen mode Exit fullscreen mode

Utilizaremos las variables de entorno de Vite para obtener el modo actual de nuestro proyecto.

3. Crear .env

Ahora creamos el archivo .env en la raíz de nuestro proyecto y colocamos lo siguiente:

VITE_APP_ENV=development
VITE_API_URL=https://nuestra-api.com
VITE_API_URL_LOCAL=http://localhost:3030
Enter fullscreen mode Exit fullscreen mode

Esto básicamente nos permite llamar dinámicamente a la API correspondiente dependiendo del modo actual del proyecto. Esto también nos serviría si tenemos múltiples API's o una API que no sea definitiva o que esté en constante cambio, la agregaríamos al .env y únicamente tendríamos que preocuparnos de reemplazarla ahí.

Ahora simplemente en el archivo fetchLibrary importamos la función:

import { validProductionUrl } from '../Utils/utils'

Y la función fetchApiGet que se encuentra dentro le agregamos *validProductionUrl * a la URL, quedando de la siguiente manera:

const request = await fetch(validProductionUrl({ url }), {
  headers,
  method: 'GET'
})
Enter fullscreen mode Exit fullscreen mode

Únicamente nos quedaría, en nuestro archivo orders.tsx limpiar la url que le enviamos al fetch:

const listOrders = await fetchApiGet({
   url: '/orders'
})
Enter fullscreen mode Exit fullscreen mode

Porque recordemos que ahora agregamos la url de forma dinámica a través del .env, entonces al enviar la url así:

/orders

La función nos la devuelve así:

https://nuestra-api/orders

O así:

http://localhost:3030/orders


Manejo de versionado desde variables de entorno

Siguiendo con el punto anterior, agregaremos un punto más, muy sencillo pero nos ayudará a hacer las consultas más ordenadas y escalables.

Agrega esto a tu .env ya creado:

VITE_API_VERSION=/api/v1

Ahora en nuestro fetchApiGet agregamos el versionado a la URL:

const request = await fetch(validProductionUrl({ url: `${import.meta.env.VITE_API_VERSION}${url}` }), {
      headers,
      method: 'GET'
})
Enter fullscreen mode Exit fullscreen mode

Podríamos agregar esa importación a una función en el utils.tsx de la siguiente manera:

export function getApiVersion(): string {
  return import.meta.env.VITE_API_VERSION
}
Enter fullscreen mode Exit fullscreen mode

Ahora quedaría un poco más limpio:

const request = await fetch(validProductionUrl({ url: `${getApiVersion()}${url}` }), {
      headers,
      method: 'GET'
})
Enter fullscreen mode Exit fullscreen mode

Y nuestro request final ahora se enviaría:

https://nuestra-api/api/v1/orders

Por supuesto tendríamos que asegurarnos de que nuestra API reciba el request a /api/v1/orders y no únicamente a /orders.


Manejo de respuestas con MessageHandler

Este Custom Hook nos permite mantener un orden en cuanto a las respuestas de nuestras consultas, tanto para los success como errors.

1. Crear archivo

- src
   - Hooks
      - handlerMessages
Enter fullscreen mode Exit fullscreen mode
import { toast } from 'sonner'

export function useMessageHandler () {
  const errorHandler = ({ errorMessage, data, show }: { errorMessage: string, data: object, show: boolean }): Error => {
    if (import.meta.env.VITE_APP_ENV !== 'production') {
      console.error(data)
    }
    if (show) {
      toastHandler({ message: errorMessage, type: 'error' })
    }
    throw new Error(errorMessage)
  }

  const successHandler = ({ successMessage, data, show }: { successMessage: string, data: object, show: boolean }): void => {
    if (import.meta.env.VITE_APP_ENV !== 'production') {
      console.log(data)
    }
    if (show) {
      toastHandler({ message: successMessage, type: 'success' })
    }
  }

  const toastHandler = ({ message, type }: { message: string, type: 'success' | 'error' | 'info' | 'warning' }): void => {
    toast[type](message)
  }

  return { errorHandler, successHandler, toastHandler }
}
Enter fullscreen mode Exit fullscreen mode

Este Hook nos provee 3 funciones:

  • errorHandler: Maneja los errores.
  • successHandler: Maneja los success. Estas dos funciones reciben el texto a mostrar, data para el consog.log y boolean show para decidir si mostrar la notificación en pantalla o no.
  • toastHandler: Maneja el proceso de la notificación.

A su vez integramos la validación de producción del .env para que, si estamos en local se muestre la información en consola, pero solo si estamos en local. Recordemos que en producción NO debería quedar ningún mensaje en consola.

Podemos usar las notificaciones que queramos, yo en este ejemplo estoy utilizando los Toast de Sonner que se instalan de la siguiente manera:

npm install sonner
Enter fullscreen mode Exit fullscreen mode

Ahora simplemente en nuestro fetchApiGet o en cualquier otro sitio que necesitemos manejar errores, llamamos nuestra función de esta manera:

 if (!request.ok) {
      return errorHandler({ errorMessage: 'Network response was not ok', data: request, show: false })
    }

const response = await request.json()

return response
Enter fullscreen mode Exit fullscreen mode

Consultas seguras (JWT)

Un punto esencial, el más importante sobre la comunicación entre nuestro Front y nuestra API es sin duda la seguridad, una manera fácil y común de colocar seguridad básica a nuestros request es a través del estándar JWT.

Suponiendo nuevamente que nuestra API ya está configurada para recibir, generar y guardar tokens, nos enfocaremos en crear nuestra parte en React.

1. Creamos la petición POST

En nuestro Hook fetchLibrary crearemos nuestra nueva petición, es similar al GET que ya tenemos solo que enviaremos un body.

const fetchApiPost = async ({ url, body }: { url: string, body: object, secure: boolean }): Promise<ApiPostResponse> => {
    const headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json'
    }
    const request = await fetch(validProductionAdminUrl({ url: `${import.meta.env.VITE_API_VERSION}${url}` }), {
      headers,
      method: 'POST',
      body: JSON.stringify(body)
    })

    if (!request.ok) {
      return errorHandler({ errorMessage: 'Network response was not ok', data: request, show: false })
    }

    const response: ApiPostResponse = await request.json()

    return response
  }
Enter fullscreen mode Exit fullscreen mode

Y no nos olvidemos de exportar la función:

return { fetchApiGet, fetchApiPost }
Enter fullscreen mode Exit fullscreen mode

2. Crear funciones Cookies

En nuestro archivo utils creamos tres funciones para crear, obtener y eliminar cookies, yo utilizo la librería js-cookie.

import Cookies from 'js-cookie'

export function setCookie ({ cookie, value, expires, secure }: { cookie: string, value: any, expires: number | undefined, secure: boolean }): void {
  Cookies.set(cookie, value, { expires, secure })
}
export function getCookie ({ cookie }: { cookie: string }): string | undefined {
  return Cookies.get(cookie)
}
export function removeCookie ({ cookie }: { cookie: string }): void {
  return Cookies.remove(cookie)
}
Enter fullscreen mode Exit fullscreen mode

3. Crear el Custom Hook userAuth.

- src
   - Hooks
      - Models
         - userAuth.tsx
Enter fullscreen mode Exit fullscreen mode

Ya teniendo las tres funciones de cookies en nuestro utils, podemos crear las primeras dos funciones en nuestro nuevo Hook userAuth antes de crear la función principal del Hook.

import { fetchLibrary } from '../fetch'
import { useMessageHandler } from '../handlerMessages'
import { setCookie, getCookie, removeCookie } from '../../Utils/utils'

function setAuthToken ({ receivedToken, tokenExpiry }: { receivedToken: string, tokenExpiry?: number }): void {
  setCookie({ cookie: 'authToken', value: receivedToken, expires: tokenExpiry, secure: true })
}

export function getAuthToken (): string | undefined {
  return getCookie({ cookie: 'authToken' })
}
Enter fullscreen mode Exit fullscreen mode

En setAuthToken podemos enviarle el ´tokenExpiry´ para determinar el tiempo de vida del token, sino le enviamos ninguno, por defecto tendrá el tiempo de vida de la sesión en el navegador.

Ahora si, declaramos la interface, creamos la función Hook y creamos otras dos funciones:

interface LoginResponse {
  data: any[];
  status: number;
}

export function useUserAuth () {
  const { fetchApiPost } = fetchLibrary()
  const { errorHandler } = useMessageHandler()

  const login = async ({ email, password }: { email: string, password: string }): Promise<LoginResponse> => {
    try {
      const loginResult = await fetchApiPost({
        url: '/login',
        body: {
          email: email,
          password: password,
        },
      })

      if (loginResult instanceof Error || loginResult.success.type !== 'success' || !loginResult.data.token) {
        return errorHandler({ errorMessage: 'Success false', data: loginResult, show: true })
      }

      setAuthToken({ receivedToken: loginResult.data.token })
      return loginResult
    } catch (e) {
      return errorHandler({ errorMessage: 'Authentication failed', data: e, show: true })
    }
  }

  const logout = () => () => {
    removeCookie({ cookie: 'authToken' })
  }
  return { login, logout }
}
Enter fullscreen mode Exit fullscreen mode

Estas funciones son a modo de ejemplo, estamos mostrando el proceso de como sería el post a nuestra API con datos básicos de Login, como respuesta nos vendría el Token y luego lo guardamos en las Cookies.

Ya con esto tendríamos el sistema de asignación y eliminación de Cookies para el usuario, ahora tenemos que aplicarlo a nuestras consultas para hacerlas seguras.

4. Modificación de nuestra función fetchLibrary

Los cambios que haríamos acá serían:

  • Importar la función que acabamos de crear para obtener la Cookie.
  • Agregar nuevo header de Authorization.
import { getAuthToken } from './Models/userAuth'

const fetchApiGet = async ({ url, secure }: { url: string, secure: boolean }): Promise<ApiGetResponse> => {
    const headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      Authorization: secure ? `Bearer ${getAuthToken()}` : ''
    }
    const request = await fetch(validProductionAdminUrl({ url: `${import.meta.env.VITE_API_VERSION}${url}` }), {
      headers,
      method: 'GET'
    })

    if (!request.ok) {
      return errorHandler({ errorMessage: 'Network response was not ok', data: request, show: false })
    }

    const response: ApiGetResponse = await request.json()

    return response
  }

Enter fullscreen mode Exit fullscreen mode

Acá ya la consulta es segura, con un condicional porque puede que necesitemos hacer alguna consulta a nuestra API que no haga falta que sea segura (o que no puede) como por ejemplo, esa primera consulta ´/login´, al ser la primera consulta que se hace a nuestra API, y además al ser la consulta que nos trae el token como respuesta, aún no tenemos el token por lo cual no la podemos hacer segura.

Y ahora simplemente en nuestro Hook de Orders modificamos el fetch para que sea seguro:

const listOrders = await fetchApiGet({
 url: `/orders`,
 secure: true
})
Enter fullscreen mode Exit fullscreen mode

Envío de archivos

El envío de archivos funciona de la misma manera que la petición estándar POST, solo que tenemos que modificar una pequeña cosita antes de enviar la petición:

  const fetchApiUploadFile = async ({ url, file, secure }: { url: string, file: File, secure: boolean }): Promise<ApiPostResponse> => {
    if (!(file instanceof File)) {
      return errorHandler({ errorMessage: 'El parámetro "file" no es un objeto File válido.', data: file, show: false })
    }

    const formData = new FormData()
    formData.append('file', file)

    const headers = {
      Accept: 'application/json',
      Authorization: secure ? `Bearer ${getAuthToken()}` : ''
    }
    const request = await fetch(validProductionAdminUrl({ url: `${import.meta.env.VITE_API_VERSION}${url}` }), {
      headers,
      method: 'POST',
      body: formData
    })

    if (!request.ok) {
      return errorHandler({ errorMessage: 'Network response was not ok', data: request, show: false })
    }

    const response: ApiPostResponse = await request.json()

    return response
  }
Enter fullscreen mode Exit fullscreen mode

Esto nos permite enviar archivos a nuestra API que, por supuesto, tiene que estar condicionada para recibir un archivo. Ahora sería solo enviar el archivo en la petición del Hook, por ejemplo:

const UploadBanner = async ({ file }: { file: File }) => {
    try {
      const uploadBannerResult = await fetchApiUploadFile({
        url: `/banner`,
        secure: true,
        file
      })

      if (uploadBannerResult instanceof Error) {
        return errorHandler({ errorMessage: 'Success false', data: uploadBannerResult, show: true })
      }

      return uploadBannerResult
    } catch (e) {
      return errorHandler({ errorMessage: 'Upload Banner failed', data: e, show: true })
    }
  }
Enter fullscreen mode Exit fullscreen mode

Consulta externa

Una consulta externa sería exactamente igual a lo que ya hemos hecho, solo que no la enviaremos segura:

const fetchGetUrl = async ({ url }: { url: string }): Promise<any> => {
    const headers = {
      'Content-Type': 'application/json',
      Accept: 'application/json'
    }
    const request = await fetch(url, {
      headers,
      method: 'GET'
    })

    if (!request.ok) {
      return errorHandler({ errorMessage: 'Network response was not ok', data: request, show: false })
    }

    const response = await request.json()

    return response
  }
Enter fullscreen mode Exit fullscreen mode

Podemos definir el tipo de promesa si sabemos cuál va a hacer la respuesta estándar de las consultas.

Outro

  • Podemos crear la librería completa de todas las consultas; DELETE, GET, POST, PATCH.
  • Si hacemos consulta a una API en específico, podemos analizar la opción de crear una función específica para esa API así manejamos su respuesta adecuadamente, tal como hicimos con fetchApiGet.
  • Todo es perfectamente personalizable y adaptable, siéntase libre de probar, desarrollar y si tiene tiempo, compartir.
  • Esto es una guía básica-media, siempre se puede desarrollar más, innovar más y mejorar más.


Contacto

Top comments (0)