Cómo escribir un envoltorio de API usando clases de JavaScript y recuperación

Cómo escribir un envoltorio de API usando clases de JavaScript que llama a la API de marcador de posición JSON usando métodos convenientes y fáciles de recordar a través de Fetch.

Primeros pasos

Para este tutorial, vamos a utilizar el marco JavaScript de pila completa de CheatCode, Joystick. Joystick reúne un marco de interfaz de usuario de front-end con un back-end de Node.js para crear aplicaciones.

Para comenzar, querremos instalar Joystick a través de NPM. Asegúrese de estar usando Node.js 16+ antes de instalar para garantizar la compatibilidad (lea este tutorial primero si necesita aprender a instalar Node.js o ejecutar varias versiones en su computadora):

Terminal

npm i -g @joystick.js/cli

Esto instalará Joystick globalmente en su computadora. Una vez instalado, vamos a crear un nuevo proyecto:

Terminal

joystick create app

Después de unos segundos, verá un mensaje desconectado en cd en su nuevo proyecto y ejecute joystick start :

Terminal

cd app && joystick start

Después de esto, su aplicación debería estar ejecutándose y estamos listos para comenzar.

Escribiendo la clase contenedora API

Para este tutorial, vamos a escribir un contenedor para la API de marcador de posición JSON, una API REST HTTP gratuita para realizar pruebas. Nuestro objetivo es crear un "envoltorio" reutilizable que nos ayude a agilizar el proceso de realizar solicitudes a la API.

Para comenzar, vamos a construir el propio envoltorio de la API como una clase de JavaScript. Esto nos dará una manera de, si lo deseamos, crear múltiples instancias de nuestro contenedor. Dentro de la aplicación que acabamos de crear, abramos el /api carpeta en la raíz del proyecto y cree un nuevo archivo en /api/jsonplaceholder/index.js :

/api/jsonplaceholder/index.js

class JSONPlaceholder {
  constructor() {
    this.endpoints = {};
  }
}

export default new JSONPlaceholder();

Creando un esqueleto para nuestro contenedor, aquí configuramos una clase básica de JavaScript con un constructor() función:lo que se llama inmediatamente después del new La palabra clave se llama en una clase de JavaScript, que configura un objeto vacío en la clase this.endpoints . Adentro, a medida que avancemos, construiremos este this.endpoints objeto para contener métodos (funciones definidas en un objeto) para generar dinámicamente las solicitudes HTTP que queremos que realice nuestro contenedor.

En la parte inferior de nuestro archivo, aunque técnicamente solo podemos exportar la clase en sí (sin el new palabra clave), aquí, para probar, solo vamos a crear una sola instancia y exportarla como export default new JSONPlaceholder() . Esto nos permitirá importar y llamar a nuestro contenedor directamente desde cualquier otro lugar de nuestra aplicación sin tener que hacer algo como esto primero:

import JSONPlaceholder from 'api/jsonplaceholder/index.js';

const jsonPlaceholder = new JSONPlaceholder();

jsonPlaceholder.posts('list');

En su lugar, solo podremos hacer:

import jsonPlaceholder from './api/jsonplaceholder/index.js';

jsonPlaceholder.posts('list');

Para ver cómo llegamos a este punto, a continuación, construyamos ese this.endpoints objeto en el constructor y explicar cómo nos ayudará a realizar solicitudes.

/api/jsonplaceholder/index.js

import fetch from 'node-fetch';

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        list: (options = {}) => {
          return {
            method: 'GET',
            resource: `/posts${options.postId ? `/${options.postId}` : ''}`,
            params: {},
            body: null,
          };
        },
      },
    };
  }
}

export default new JSONPlaceholder();

Para cuando terminemos nuestro contenedor, nuestro objetivo es poder llamar a un extremo de la API como este:jsonPlaceholder.posts('list') y reciba la respuesta de JSON Placeholder API sin realizar ningún paso adicional.

Para llegar allí, necesitamos una forma estandarizada de generar las solicitudes HTTP que vamos a realizar. Esto es lo que estamos haciendo arriba. Sabemos que potencialmente necesitaremos cuatro cosas para realizar una solicitud a la API:

  1. El método HTTP admitido por el punto final de destino (es decir, POST , GET , PUT o DELETE ).
  2. El recurso o la URL del punto final.
  3. Cualquier parámetro de consulta opcional u obligatorio.
  4. Un objeto de cuerpo HTTP opcional u obligatorio.

Aquí, creamos una plantilla para especificar estas cuatro cosas. Para mantener nuestro contenedor organizado, en nuestro this.endpoints objeto, creamos otra propiedad posts que representa el recurso API para el que queremos generar una plantilla de solicitud. Anidado debajo de esto, asignamos funciones a las propiedades con nombres que describen lo que está haciendo la solicitud HTTP, devolviendo la plantilla relacionada con esa tarea.

En el ejemplo anterior, queremos recuperar una lista de publicaciones. Para hacerlo, necesitamos crear una plantilla que nos diga que realicemos un HTTP GET solicitud al /posts URL en la API de marcador de posición JSON. De forma condicional, también, debemos poder pasar el ID de una publicación a este punto final como /posts/1 o /posts/23 .

Es por eso que definimos nuestros generadores de plantillas de solicitud como funciones. Esto nos permite, si es necesario, tomar un conjunto de opciones pasadas cuando se llama al contenedor (por ejemplo, aquí, queremos tomar la ID de una publicación que anticipamos que se pasará a través de options.postId ).

A cambio de nuestra función, obtenemos un objeto que luego podemos usar en nuestro código para realizar la solicitud HTTP real. Muy rápido, construyamos el resto de nuestros generadores de plantillas de solicitud:

/api/jsonplaceholder/index.js

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => {
          return {
            method: 'POST',
            resource: `/posts`,
            params: {},
            body: {
              ...options,
            },
          };
        },
        list: (options = {}) => {
          return {
            method: 'GET',
            resource: `/posts${options.postId ? `/${options.postId}` : ''}`,
            params: {},
            body: null,
          };
        },
        post: (options = {}) => {
          if (!options.postId) {
            throw new Error('A postId is required for the posts.post method.');
          }

          return {
            method: 'GET',
            resource: `/posts/${options.postId}`,
            params: {},
            body: null,
          };
        },
        comments: (options = {}) => {
          if (!options.postId) {
            throw new Error('A postId is required for the posts.comments method.');
          }

          return {
            method: 'GET',
            resource: `/posts/${options.postId}/comments`,
            params: {},
            body: null,
          };
        },
      },
    };
  }
}

export default new JSONPlaceholder();

El mismo patrón exacto repetido, solo para diferentes puntos finales y diferentes propósitos. Para cada punto final que queramos admitir, bajo el this.endpoints.posts objeto, agregamos una función asignada a un nombre conveniente, tomando un conjunto posible de options y devolver una plantilla de solicitud como un objeto con cuatro propiedades:method , resource , params y body .

Preste mucha atención a cómo varían las plantillas según el punto final. Algunos usan diferentes method s mientras que otros tienen un body mientras que otros no. Esto es lo que entendemos por tener una plantilla estandarizada. Todos devuelven un objeto con la misma forma, sin embargo, lo que ponen en ese objeto difiere según los requisitos del punto final al que intentamos acceder.

También debemos llamar la atención sobre el this.endpoints.posts.post plantilla y el this.endpoints.posts.comments modelo. Aquí arrojamos un error si options.postId no está definido ya que se requiere una ID de publicación para cumplir con los requisitos de estos puntos finales.

A continuación, tenemos que poner estos objetos en uso. Recuerde, nuestro objetivo es llegar al punto en el que podamos llamar a jsonPlaceholder.posts('list') en nuestro código y obtener una lista de publicaciones. Ampliemos un poco nuestra clase para incluir el .posts() parte de esa línea y vea cómo hace uso de nuestras plantillas de solicitud.

/api/jsonplaceholder/index.js

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => { ... },
        list: (options = {}) => { ... },
        post: (options = {}) => { ... },
        comments: (options = {}) => { ... },
      },
    };
  }

  posts(method = '', options = {}) {
    const existingEndpoint = this.endpoints.posts[method];

    if (existingEndpoint) {
      const endpoint = existingEndpoint(options);
      return this.request(endpoint);
    }
  }
}

export default new JSONPlaceholder();

Esto debería hacer las cosas un poco más claras. Aquí, hemos agregado un método a nuestro JSONPlaceholder clase posts que acepta dos argumentos:method y options . El primero, method , se asigna a una de nuestras plantillas mientras que la segunda, options , es donde podemos pasar valores condicionalmente para nuestro punto final (por ejemplo, como vimos con el ID de la publicación anteriormente al definir nuestras plantillas).

Mirando el cuerpo de ese posts() método, comenzamos comprobando si this.endpoints.posts tiene una propiedad con un nombre que coincide con el method pasado argumento. Por ejemplo, si method es igual a list la respuesta sería "sí", pero si method es igual a pizza , no lo haría.

Esto es importante. No queremos intentar llamar a un código que no existe. Usando la variable existingEndpoint , si obtenemos un valor a cambio como existingEndpoint (esperamos que esto sea una función si se usa un nombre válido), luego, queremos llamar a esa función para recuperar nuestro objeto de plantilla de solicitud. Observe que cuando llamamos a la función almacenada en existingEndpoint , pasamos el options objeto.

Para que quede claro, considera lo siguiente:

jsonPlaceholder.posts('list', { postId: '5' });

Llamamos a nuestro contenedor pasando un postId establecido en '5' .

const existingEndpoint = this.endpoints.posts['list'];

A continuación, porque method era igual a list , recuperamos el this.endpoints.posts.list función.

(options = {}) => {
  return {
    method: 'GET',
    resource: `/posts${options.postId ? `/${options.postId}` : ''}`,
    params: {},
    body: null,
  };
}

A continuación, dentro de esa función, vemos que options.postId está definido e incrustarlo en la URL del recurso como /posts/5 .

/api/jsonplaceholder/index.js

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => { ... },
        list: (options = {}) => { ... },
        post: (options = {}) => { ... },
        comments: (options = {}) => { ... },
      },
    };
  }

  posts(method = '', options = {}) {
    const existingEndpoint = this.endpoints.posts[method];

    if (existingEndpoint) {
      const endpoint = existingEndpoint(options);
      return this.request(endpoint);
    }
  }
}

export default new JSONPlaceholder();

Finalmente, de vuelta en nuestro posts() método, esperamos obtener un endpoint que es el objeto de plantilla de solicitud que generamos dentro de this.endpoints.posts.list .

A continuación, justo debajo de esto, llamamos a otro método que debemos definir:this.request() , pasando el endpoint objeto que recibimos de this.endpoints.posts.list . Echemos un vistazo a esa función ahora y terminemos nuestro contenedor.

/api/jsonplaceholder/index.js

import fetch from 'node-fetch';

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => { ... },
        list: (options = {}) => { ... },
        post: (options = {}) => { ... },
        comments: (options = {}) => { ... },
      },
    };
  }

  request(endpoint = {}) {
    return fetch(`https://jsonplaceholder.typicode.com${endpoint.resource}`, {
      method: endpoint?.method,
      body: endpoint?.body ? JSON.stringify(endpoint.body) : null,
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      return error;
    });
  }

  posts(method = '', options = {}) {
    const existingEndpoint = this.endpoints.posts[method];

    if (existingEndpoint) {
      const endpoint = existingEndpoint(options);
      return this.request(endpoint);
    }
  }
}

export default new JSONPlaceholder();

Muy rápido, antes de ver el nuevo request() arriba, observe que hemos agregado un paquete NPM como dependencia:node-fetch . Instalémoslo en nuestra aplicación antes de continuar:

Terminal

npm i node-fetch

A continuación, echemos un vistazo más de cerca a este request() método:

/api/jsonplaceholder/index.js

import fetch from 'node-fetch';

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => { ... },
        list: (options = {}) => { ... },
        post: (options = {}) => { ... },
        comments: (options = {}) => { ... },
      },
    };
  }

  request(endpoint = {}) {
    return fetch(`https://jsonplaceholder.typicode.com${endpoint.resource}`, {
      method: endpoint?.method,
      body: endpoint?.body ? JSON.stringify(endpoint.body) : null,
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      return error;
    });
  }

  posts(method = '', options = {}) {
    const existingEndpoint = this.endpoints.posts[method];

    if (existingEndpoint) {
      const endpoint = existingEndpoint(options);
      return this.request(endpoint);
    }
  }
}

export default new JSONPlaceholder();

Ahora viene la parte divertida. Dentro del request() método, nuestro objetivo es aceptar el objeto de plantilla de solicitud como endpoint y utilícelo para adaptar la solicitud HTTP que hacemos a la API de marcador de posición JSON.

Mirando ese método, return una llamada al fetch método que estamos importando desde el node-fetch paquete que acabamos de instalar. A él le pasamos la URL a la que queremos hacer nuestra petición HTTP. Aquí, la URL "base" para la API es https://jsonplaceholder.typicode.com . Mediante la interpolación de cadenas de JavaScript (indicada por los acentos graves que usamos para definir nuestra cadena en lugar de comillas simples o dobles), combinamos esa URL base con el endpoint.resource valor de la plantilla que coincide con la llamada.

Por ejemplo, si llamamos a jsonPlaceholder.posts('list') esperaríamos la URL que pasamos a fetch() ser https://jsonplaceholder.typicode.com/posts . Si llamamos a jsonPlaceholder.posts('list', { postId: '5' }) , esperamos que la URL sea https://jsonplaceholder.typicode.com/posts/5 .

Siguiendo esta lógica, después de la URL, pasamos un objeto a fetch() que contiene opciones adicionales para la solicitud. Aquí, hacemos uso del .method propiedad en la plantilla pasada y, condicionalmente, el .body propiedad en la plantilla pasada. Si .body está definido, tomamos el valor que contiene y lo pasamos a JSON.stringify() —una función de JavaScript incorporada— para convertir el objeto en una cadena (importante ya que solo podemos pasar un valor de cadena para el cuerpo de la solicitud HTTP, no para el objeto sin procesar).

Después de esto, al final de nuestra llamada a fetch() encadenamos un .then() función de devolución de llamada como esperamos fetch() para devolver una promesa de JavaScript. A .then() pasamos nuestra función de devolución de llamada, anteponiendo el async palabra clave para decirle a JavaScript que "nos gustaría usar el await palabra clave para una de las funciones que llamamos dentro de esta función" (sin esto, JavaScript generaría un error diciendo await era una palabra clave reservada).

Tomando el response pasado a esa función de devolución de llamada, esta es la respuesta HTTP de la API de marcador de posición JSON, llamamos a su .json() método, colocando await al frente como esperamos response.json() para devolver una promesa de JavaScript. Usamos .json() aquí porque queremos convertir el texto sin formato HTTP response body que recuperamos de la API en datos JSON que podemos usar en nuestro código.

Almacenando este resultado en el data variable, la devolvemos desde el .then() devolución de llamada que regresará al return declaración delante de fetch() y luego vuelve a subir una vez más al return declaración delante de this.request() dentro del posts() método (donde se originó nuestra llamada). A su vez, esto significa que esperamos obtener nuestro data para salir así:

const data = await jsonPlaceholder.posts('list');
console.log(data);
/*
[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
  {
    "userId": 1,
    "id": 3,
    "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
    "body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
  },
]
*/

Eso lo hace por nuestro envoltorio. Ahora, para ver esto en acción, conectaremos algunas rutas de prueba a las que podemos acceder a través de un navegador web, llamando a nuestro contenedor para verificar las respuestas.

Definición de rutas para probar el contenedor

Para probar nuestro envoltorio de API, ahora vamos a conectar algunas rutas en nuestra propia aplicación que llamará a la API de marcador de posición JSON a través de nuestro envoltorio y luego mostrará los datos que obtengamos en nuestro navegador.

/index.servidor.js

import node from "@joystick.js/node";
import api from "./api";
import jsonPlaceholder from "./api/jsonplaceholder";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/posts/create": async (req, res) => {
      const post = await jsonPlaceholder.posts('create', { title: 'Testing Posts' });
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(post, null, 2));
    },
    "/posts": async (req, res) => {
      const posts = await jsonPlaceholder.posts('list');
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(posts, null, 2));
    },
    "/posts/:postId": async (req, res) => {
      const post = await jsonPlaceholder.posts('post', { postId: req?.params?.postId });
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(post, null, 2));
    },
    "/posts/:postId/comments": async (req, res) => {
      const comments = await jsonPlaceholder.posts('comments', { postId: req?.params?.postId });
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(comments, null, 2));
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

Esto puede parecer abrumador, pero mire de cerca. Dentro de nuestra aplicación, cuando ejecutamos joystick create antes, un index.server.js El archivo se configuró para nosotros donde se inicia el servidor Node.js para nuestra aplicación. En ese archivo, node.app() configura un servidor Express.js detrás de escena y toma el routes objeto lo pasamos para generar dinámicamente rutas Express.js.

Aquí, hemos agregado algunas rutas de prueba a ese objeto, cada una de las cuales corresponde a uno de los métodos en nuestro contenedor API. Además, en la parte superior de index.server.js , hemos importado nuestro jsonPlaceholder contenedor (recuerde, esperamos que sea una instancia preiniciada de nuestro JSONPlaceholder clase).

Centrándonos en nuestras rutas, empezando por /posts/create , aquí, comenzamos pasando una función que representa nuestro controlador de ruta con el async palabra clave antepuesta (nuevamente, esto le dice a JavaScript que nos gustaría usar el await palabra clave dentro de la función que sigue a esa declaración).

Aquí, creamos una variable post establecer igual a una llamada a await jsonPlaceholder.posts('create', { title: 'Testing Posts' }) . Como acabamos de aprender, si todo funciona bien, esperamos que esto genere la plantilla para nuestra solicitud HTTP a la API de marcador de posición JSON y luego realice la solicitud a través de fetch() , devolviéndonos el .json() datos analizados de la respuesta. Aquí, almacenamos esa respuesta como post y luego hacer dos cosas:

  1. Establecer el HTTP Content-Type encabezado en la respuesta a nuestra ruta Express.js a application/json para indicarle a nuestro navegador que el contenido que le enviamos son datos JSON.
  2. Responder a la solicitud de nuestra ruta con una versión en cadena de nuestro posts respuesta (formateada para usar dos tabuladores/espacios).

Si abrimos un navegador web, deberíamos ver algo como esto al visitar http://localhost:2600/posts/create :

¿Guay, verdad? Esto funciona como si escribiéramos todo el código para realizar un fetch() solicitud dentro de nuestra función de controlador de ruta, ¡pero solo nos tomó una línea de código para hacer la llamada!

Si miramos de cerca nuestras rutas anteriores, todas funcionan más o menos igual. Observe la variación entre cada ruta y cómo eso cambia nuestra llamada a jsonPlaceholder.posts() . Por ejemplo, mirando el /posts/:postId/comments ruta, aquí utilizamos el comments método que conectamos que requiere un postId pasado en el objeto de opciones de nuestra llamada contenedora. Para pasarlo, aquí tiramos del postId de los parámetros de nuestra ruta y pasarlo al objeto de opciones del contenedor como postId . A cambio, recibimos los comentarios de la publicación correspondientes a la ID que especificamos en nuestra URL:

Impresionante. Muy rápido, hagamos un repaso en vivo de todas nuestras rutas antes de darle nuestro sello de aprobación:

Y ahí lo tenemos. Un envoltorio de API completamente funcional. Lo bueno de este patrón es que podemos aplicarlo a cualquier API HTTP o REST cuyo uso nos gustaría estandarizar.

Terminando

En este tutorial, aprendimos cómo crear un contenedor de API utilizando una clase de Javascript. Escribimos nuestro contenedor para JSON Placeholder API, aprendiendo cómo usar un enfoque basado en plantillas para generar solicitudes y aprovechar una sola función para realizar esa solicitud a través de fetch() . También aprendimos cómo definir métodos específicos de recursos en nuestra clase para hacer que nuestro contenedor sea extensible y fácil de usar.