Taller: React con hooks III

En esta sesión veremos los servicios y el api rest.

Catrin Welz-Stein

archivado en: JavaScript / 25 marzo, 2020

Seguimos con este taller de react con hooks para pasar la cuarentena. Vamos con los servicios, que también vamos a preparar como si estuviéramos realizando una aplicación grande.

La idea es la siguiente. Vamos a centralizar todas las llamadas en un archivo, que podemos llamar api-rest o similar, en el directorio core. Desde ahí se realizarán lo que son las llamadas en sí mismas, de tal manera que si en el futuro se decide usar otro "lanzador", como ocurrió en una de las releases de angular 2*, solo se deba tocar en un sitio.

Este lanzador cogerá la configuración de un archivo externo, que en la vida real podría venir suministrado desde un servicio o estar incluido dentro de un ciclo de integración continua que prepare builds específicas para los distintos entornos (dev, pre, pro, test).

Los servicios, como la llamada que recupera el listado de perros, irán contra nuestro sistema de api rest y se gestionan en archivos específicos desde los componentes, que enviarán o no los datos al store si fuera necesario, tal y como veremos en otra entrada.

Para embridar todo ese maremagnum de datos que se van a manejar y que en todos los sitios se hable, por ejemplo, de products y no en un sitio de prods y en otro de productos, preparemos distintas interfaces typescript, que podemos guardar en un directorio llamado schemas o models o similar.

Comenzamos con el archivo de configuración. Solo lleva unas pocas constantes, pero en la vida real suelen tener muchas más, como la versión del api rest, distintas bases url por entorno, etcétera.

// src/core/config/constants.ts

interface constants {
  ENVIRONMENT: 'dev' | 'prod';
  BASE_URL: {
    dev: string;
    prod: string;
  },
  API_VERSION: string;
}

const constants: constants = {
  ENVIRONMENT: 'dev',
  BASE_URL: {
    dev: 'https://dog.ceo',
    prod: 'https://dog.ceo'
  },
  API_VERSION: 'api'
};

export default constants;

Luego preparamos los interfaces que nos permitirán manejar los datos de forma coherente entre los distintos scripts. En un archivo ponemos tipamos las respuestas genéricas de los end points.

// src/schemas/responses.ts

export interface IntResponses {
  message: any;
  status: string;
}

Y en otro agrupamos las interfaces relacionadas con el tipo de datos de los perros. Si hubiera otro grupo de datos, como los de un usuario (nombre, rol...), irían en un archivo aparte.

// src/schemas/dogs.ts

export interface IntDog {
  name: string;
  species: Array<string>;
}

export interface IntDogSelected {
  dog: string;
  specie: string;
  images: Array<string>;
  species: Array<string>;
}

Seguimos con el api rest, en el que usaré para este taller la librería axios, aunque se puede preparar con fetch, las llamadas ajax tradicionales combinadas con promesas o lo que sea.

Instalamos axios.

npm install axios --save

Y preparamos nuestra api rest. Para este ejemplo usaré el patrón reveal, un clásico que permite definir métodos públicos y privados. Se puede usar el que sea. Luego vemos cómo conseguir lo mismo mediante funciones. En este objeto tendremos los métodos CRUD (coger, actualizar, borrar y crear), uno privado para preparar las urls y otro público para saber si la respuesta del servidor ha sido correcta.

// src/core/rest/api-rest.ts

import axios from 'axios';
import constants from '../config/constants';
import { IntResponses } from '../../schemas/responses';

interface IntApiRest {
  _getUrl(_url_: string): string;
  get(_url_: string): Promise<any>;
  post(_url_: string, payload?: any): Promise<any>;
  put(_url_: string, payload?: any): Promise<any>;
  delete(_url_: string): Promise<any>;
  isSuccessResponse(response: IntResponses) : boolean;
}

const apiRest = ( ()=> {

  const module = {} as IntApiRest;
  const self = module;

  module._getUrl = (_url_) => {
    return `${constants.BASE_URL[constants.ENVIRONMENT]}/${constants.API_VERSION}/${_url_}`;
  }

  module.get = (_url_) => {
    const url = self._getUrl(_url_);
    return axios.get(url);
  };

  module.post = (_url_, payload) => {
    const url = self._getUrl(_url_);
    return axios.post(url, payload);
  };

  module.put = (_url_, payload) => {
    const url = self._getUrl(_url_);
    return axios.put(url, payload);
  };

  module.delete = (_url_) => {
    const url = self._getUrl(_url_);
    return axios.delete(url);
  };

  module.isSuccessResponse = (response) => {
    if (response.hasOwnProperty('status') &&
    response.status === 'success' &&
    response.hasOwnProperty('message')) {
      return true;
    }
    return false;
  };

  // Solo es accesible lo que se retorna.
  return {
    get: module.get,
    post: module.post,
    put: module.put,
    delete: module.delete,
    isSuccessResponse: module.isSuccessResponse
  }

})();

export default apiRest;

Ahora en un archivo específico ponemos todas las funciones relacionados con las llamadas de los perros. Una para mapear los datos que llegan (parseDogs), otra para recuperar el listado de perros (getDogs) y una tercera para el listado de imágenes de un perro en concreto (getDogImages). Estos dos últimos devuelven una promesa, ya que, recordemos, las llamadas ajax son asíncronas. En este caso, para conseguir el mismo efecto que el patrón reveal, es decir, para conseguir que haya funciones / métodos público y privados, cumpliendo así la ley de Deméter, hacemos una función dogsService que devuelve las funciones que necesitamos que sean públicas. Y esta función es la única que exportamos.

// src/core/services/dogs-services.ts

import apiRest from '../rest/api-rest';
import { IntDog } from '../../schemas/dogs';
import { IntResponses } from '../../schemas/responses';

const parseDogs = (resp: IntResponses): Array<IntDog> => {
  const dogs = [] as Array<IntDog>;

  if (apiRest.isSuccessResponse(resp)) {
    Object.entries(resp.message).forEach(([key, value]) => {
      let temp = {} as IntDog;

      temp.name = key;
      temp.species = Array.isArray(value) ? value : [];

      dogs.push(temp);
    });
  }

  return dogs;
}

const getDogs = (): Promise<Array<IntDog> | string> => {
  return new Promise( (resolve, reject) => {
    apiRest.get('breeds/list/all').then( (resp) => {
      const dogsMapped = parseDogs(resp.data);

      if (dogsMapped.length) {
        resolve(dogsMapped);

      } else {
        reject('error');
      }
    }).catch( () => {
      reject('error');
    })
  });
};

const getDogImages = (url: string): Promise<Array<string> | string> => {
  return new Promise( (resolve, reject) => {
    apiRest.get(`breed/${url}/images`).then( (resp) => {
      if (apiRest.isSuccessResponse(resp.data) && Array.isArray(resp.data.message)) {
        resolve(resp.data.message);

      } else {
        reject('error');
      }
    }).catch( () => {
      reject('error');
    })
  });
};

const dogsService = () => {
  return {
    getDogs,
    getDogImages
  }
};

export default dogsService;

Nota: Si hubiera end points de otra naturaleza -usuarios, gatos, lavadoras- sus servicios irían en su propio archivo.

Para terminar esta sesión, simplemente vamos a comprobar que todo funciona consoleando la llamada de getDogs en la vista view-dogs.

import React from 'react';
/* Importamos el servicio */
import dogsService from '../../core/services/dogs-services';

const ViewDogs: React.FC = () => {

  /* destructuramos las funciones que devuelve la función del servicio dogsService */
  const { getDogs, getDogImages } = dogsService();

  // Esto lo borraremos luego. Just for test ; )
  getDogs().then((data) => {
    console.log(data)
  })

  return (
    <section>
      Dogs works
    </section>
  );
}

export default ViewDogs;

Si todo ha ido bien, nos debería aparecer el listado de chuchos por consola.

Mañana comenzamos a darle forma y, ya sí, vemos los hooks. Por hoy lo dejamos aquí.

|| Tags: , ,

Este artículo aún no ha sido valorado.

¿Te ha parecido útil o interesante esta entrada?
dormido, valoración 1 nadapensativo, valoración 2 un poco sonrisa, valoración 3 a medias guiño, valoración 4 bastante aplauso, valoración 5 mucho

Tú opinión es muy importante, gracias por compartirla!

Aportar un comentario

*