REACT - Consultas a API [Completo]
Tags: react, webdev, javascript, tutorial
Published: 2024-02-05 12:33:21

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:
* [Creación de Custom Hook para Fetch’s.](#custom-hook)
* [Consulta Básica.](#get-request)
* [Validar URL para producción.](#urlvalidation)
* [Manejo de versionado desde variables de entorno.](#env-version)
* [Manejo de respuestas con MessageHandler.](#message-handler)
* [Consultas seguras (JWT).](#jwt)
* [Envío de archivos.](#file-upload)
* [Consulta externa.](#external-request)
* [Outro.](#outro)

---

## Custom Hook <a name="custom-hook"></a>

Suponiendo que ya tenemos un proyecto creado y configurado (sino, utilizaremos [Vite](https://vitejs.dev/guide) 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
```

#### 2. Inicializar el archivo

```javascript
export function fetchLibrary () {

  return {}
}
```

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

---

## Consulta GET <a name="get-request"></a>

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

```javascript
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;
  };
```

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:

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

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


  return {fetchApiGet}
}
```

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
```

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

```javascript
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 }
```

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:

```javascript
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)
      })
  }
}
```

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 <a name="urlvalidation"></a>

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
```

#### 2. Creamos la función

```javascript
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}`
  }
}
```

Utilizaremos las [variables de entorno de Vite](https://vitejs.dev/guide/env-and-mode) 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
```

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:

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

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

```javascript
const listOrders = await fetchApiGet({
   url: '/orders'
})
```
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 <a name="env-version"></a>

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:

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

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

```javascript
export function getApiVersion(): string {
  return import.meta.env.VITE_API_VERSION
}
```

Ahora quedaría un poco más limpio:

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

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 <a name="message-handler"></a>

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
```

```javascript
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 }
}
```

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](https://sonner.emilkowal.ski/) que se instalan de la siguiente manera:

```
npm install sonner
```

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

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

const response = await request.json()

return response
```

## Consultas seguras (JWT) <a name="jwt"></a>

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](https://jwt.io/).

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.

```javascript
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
  }
```
Y no nos olvidemos de exportar la función:

```javascript
return { fetchApiGet, fetchApiPost }
```

#### 2. Crear funciones Cookies
En nuestro archivo **utils** creamos tres funciones para crear, obtener y eliminar cookies, yo utilizo la librería [js-cookie](https://www.npmjs.com/package/js-cookie).

```javascript
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)
}
```

#### 3. Crear el Custom Hook userAuth.

```
- src
   - Hooks
      - Models
         - userAuth.tsx
```

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.

```javascript
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' })
}
```

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:

```javascript
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 }
}
```

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.

```javascript
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
  }

```

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:

```javascript
const listOrders = await fetchApiGet({
 url: `/orders`,
 secure: true
})
```

## Envío de archivos <a name="file-upload"></a>

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:

```javascript
  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
  }
```

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:

```javascript
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 })
    }
  }
```

## Consulta externa <a name="external-request"></a>

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

```javascript
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
  }
```
Podemos definir el tipo de promesa si sabemos cuál va a hacer la respuesta estándar de las consultas.

## Outro <a name="outro"></a>

- 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.

---

- [Vite](https://vitejs.dev/guide).
- [Variables de entorno de Vite](https://vitejs.dev/guide/env-and-mode).
- [Toast de Sonner](https://sonner.emilkowal.ski/).
- [JWT](https://jwt.io/).
- [js-cookie](https://www.npmjs.com/package/js-cookie)
- [Javascript Cookies W3](https://www.w3schools.com/js/js_cookies.asp).

---

### Contacto

- [LinkedIn](https://www.linkedin.com/in/christian-mari%C3%B1o-mazzara/)
- [GitHub](https://github.com/MazzaraCM)