Cómo usar la API de obtención de JavaScript para realizar solicitudes HTTP

Cómo usar la API de obtención de JavaScript para realizar solicitudes HTTP en el navegador y en Node.js.

Primeros pasos

Para este tutorial, vamos a utilizar CheatCode Next.js Boilerplate para mostrar el uso de fetch en el cliente y el código estándar del servidor CheatCode Node.js para mostrar el uso de fetch en el servidor.

Para comenzar, clonemos el modelo estándar de Next.js:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate client

A continuación, cd en el proyecto e instalar sus dependencias:

Terminal

cd client && npm install

Después de esto, continúe e inicie el servidor de desarrollo:

Terminal

npm run dev

A continuación, en otra pestaña o ventana de terminal, queremos clonar el modelo estándar de Node.js:

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate server

A continuación, cd en el proyecto e instalar las dependencias:

Terminal

cd server && npm install

Antes de iniciar el servidor de desarrollo, necesitamos instalar dos dependencias adicionales:isomorphic-fetch y faker :

Terminal

npm i isomorphic-fetch faker

Con esos dos instalados, continúe e inicie el servidor:

Terminal

npm run dev

Con eso, estamos listos para comenzar.

Usando la API Fetch en Node.js

Aunque puede parecer un poco al revés, para este tutorial, vamos a comenzar nuestro trabajo en el lado del servidor y luego pasar al cliente. El motivo es que vamos a configurar algunas rutas de prueba que podemos realizar fetch solicitudes en contra del cliente. Mientras estemos allí, también echaremos un vistazo rápido a cómo usar fetch en un entorno de servidor Node.js.

/servidor/api/index.js

import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.get("/users", (req, res) => {
    // We'll implement an HTTP GET test route here...
  });

  app.post("/users", (req, res) => {
    // We'll implement an HTTP POST test route here...
  });

  app.get("/photos", (req, res) => {
    // We'll implement a server-side fetch request here...
  });
};

Dentro del modelo estándar de Node.js que clonamos arriba, un servidor Express.js ya está configurado para nosotros. En el archivo anterior, el repetitivo configura las diversas API que admite (de manera predeterminada, solo una API de GraphQL). Pasado a la función que se exporta desde este archivo es Express app instancia que se configura para nosotros en el /index.js archivo en el proyecto.

Aquí, debajo de la llamada a la función donde configuramos nuestro servidor GraphQL graphql() (no usaremos esto, solo lo llamaremos para evitar confusiones), definimos tres rutas:

  1. /users usando app.get() que crea una ruta Express.js que solo acepta solicitudes HTTP GET.
  2. /users usando app.post() que crea una ruta Express.js que solo acepta solicitudes HTTP POST.
  3. /photos usando app.get() cuál es una ruta Express.js que solo acepta solicitudes HTTP GET y será donde usemos fetch para obtener datos de una API de terceros.

/servidor/api/index.js

import faker from "faker";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.get("/users", (req, res) => {
    const users = [...Array(50)].map(() => {
      return {
        name: {
          first: faker.name.firstName(),
          last: faker.name.lastName(),
        },
        emailAddress: faker.internet.email(),
        address: {
          street: faker.address.streetAddress(),
          city: faker.address.city(),
          state: faker.address.state(),
          zip: faker.address.zipCode(),
        },
      };
    });

    res.status(200).send(JSON.stringify(users, null, 2));
  });

  app.post("/users", (req, res) => {
    // We'll implement an HTTP POST test route here...
  });

  app.get("/photos", (req, res) => {
    // We'll implement a server-side fetch request here...
  });
};

Agregar un import arriba para el faker dependencia que instalamos anteriormente, aquí, estamos completando el app.get() versión de nuestro /users ruta. En el interior, nuestro objetivo es devolver algunos datos de prueba (realizaremos un fetch solicitud del cliente más tarde y esperar estos datos a cambio). Para nuestros datos, estamos usando un pequeño truco de JavaScript.

El [...Array(50)] que estamos mapeando aquí dice "cree una nueva matriz de JavaScript en la memoria con 50 elementos (estos serán solo undefined valores) y luego 'difundir' o 'descomprimir' esa matriz, usando el ... operador de propagación:en la matriz que envuelve esa declaración". Nuestro objetivo aquí es obtener 50 "marcadores de posición" que podemos reemplazar usando un JavaScript .map() método.

Vemos que eso sucede aquí, devolviendo un objeto que describe un usuario inventado para cada uno de los 50 elementos de marcador de posición. A su vez, esto nos devolverá una matriz con 50 objetos de usuario inventados. Para "maquillar" a esos usuarios, usamos el faker biblioteca, una herramienta para crear datos de prueba falsos, para crear un usuario de prueba realista para cada iteración de nuestro mapa (obtenga más información sobre la API de Faker aquí).

Finalmente, después de haber creado nuestra matriz de users , tomamos esa variable y usamos el res objeto de Express.js (esto se pasa como el segundo argumento a la función de devolución de llamada para nuestra ruta), y haga dos cosas:

  1. Establezca el código de estado HTTP en 200 usando el .status() (este es el código HTTP estándar para "éxito").
  2. Usando la capacidad de "encadenar" métodos, llama al .send() método después de establecer el .status() en res , pasando una versión en cadena de nuestro users variable (que contiene nuestra matriz de usuarios).

Aquí, usando JSON.stringify() es necesario porque solo se pueden enviar cadenas en respuesta a solicitudes HTTP. Más tarde, en el cliente, aprenderemos cómo convertir esa cadena nuevamente en una matriz de JavaScript.

/servidor/api/index.js

import faker from "faker";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.get("/users", (req, res) => {
    ...
    res.status(200).send(JSON.stringify(users, null, 2));
  });

  app.post("/users", (req, res) => {
    console.log(req.body);
    res.status(200).send(`User created!`);
  });

  app.get("/photos", (req, res) => {
    // We'll implement a server-side fetch request here...
  });
};

A continuación, para el app.post() versión de nuestro /users ruta, mantenemos las cosas simples. Porque la intención de una solicitud HTTP POST es crear o insertar algunos datos en una base de datos (o transferirlos a otra fuente de datos), aquí, solo estamos desconectando el contenido de req.body que es el contenido analizado que se nos envía a través de la solicitud. Esto será útil más adelante, ya que veremos cómo las opciones que pasamos a un fetch() solicitar determinar si el cuerpo que pasamos al cliente llega o no al servidor.

Finalmente, aquí, repetimos el mismo patrón que vimos en el app.get() versión de /users , llamando al res , configurando el .status() a 200 y enviar una respuesta de cadena (aquí, solo una cadena simple que significa la recepción del usuario).

/servidor/api/index.js

import faker from "faker";
import fetch from "isomorphic-fetch";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.get("/users", (req, res) => {
    ...
    res.status(200).send(JSON.stringify(users, null, 2));
  });

  app.post("/users", (req, res) => {
    console.log(req.body);
    res.status(200).send(`User created!`);
  });

  app.get("/photos", (req, res) => {
    fetch("https://jsonplaceholder.typicode.com/photos").then(
      async (response) => {
        const data = await response.json();
        res.status(200).send(JSON.stringify(data.slice(0, 50)));
      }
    );
  });
};

Para nuestra ruta final, creamos otro app.get() ruta, esta vez usando la ruta /photos . Para esta ruta, vamos a utilizar un fetch() del lado del servidor llamar a una API de terceros y enviar los datos que recibimos al lado del cliente de nuestra aplicación. Arriba, puedes ver que hemos importado el isomorphic-fetch dependencia que instalamos anteriormente como fetch .

Aquí, hacemos una llamada al /photos endpoint en la JSON Placeholder API gratuita que nos devuelve una serie de objetos con punteros a fotografías de stock.

Después de nuestra llamada al fetch() , encadenamos en un .then() devolución de llamada:esto significa que esperamos fetch() para devolver una promesa de JavaScript:pasar una función a ese .then() método. Dentro de esa función, tomamos el response a nuestra solicitud como argumento, agregando también un async palabra clave antes de nuestra función.

Hacemos esto porque en la siguiente línea hacemos una llamada a await frente a una llamada a response.json() . La idea aquí es que response no nos es entregado por fetch() en cualquier formato específico. En su lugar, tomamos el response sin procesar y usando uno de los pocos métodos en ese response objeto, convertimos la respuesta al formato que queremos/necesitamos.

Aquí, response.json() está diciendo para convertir el response en un formato JSON. Usamos el await aquí porque esperamos response.json() (y sus métodos hermanos como response.text() ) para devolver una promesa de JavaScript. Con un await , estamos diciendo "espere hasta que esta función nos haya devuelto un valor que podamos establecer en nuestro data variable y luego continúe con la siguiente línea".

En la siguiente línea, vemos una llamada familiar a res.status(200).send() , asegurándose de JSON.stringify() nuestros datos antes de devolverlos a la solicitud realizada desde el lado del cliente de nuestra aplicación.

¡Eso lo hace por el servidor! A continuación, vamos a saltar al cliente y ver cómo fetch() funciona en el navegador.

Uso de la API Fetch en el navegador

Pasando al modelo estándar de Next.js que clonamos anteriormente, para comenzar, vamos a utilizar la función de enrutamiento basado en páginas de Next.js para crear una nueva ruta en el cliente donde podemos probar nuestro fetch() llamadas:

/cliente/páginas/index.js

import React, { useState } from "react";

const Index = () => {
  const [data, setData] = useState([]);

  const getRequestWithFetch = (resource = "") => {
    // We'll make our GET requests using fetch here...
  };

  const postRequestWithFetch = () => {
    // We'll make a our POST request using fetch here...
  };

  return (
    <div>
      <button
        className="btn btn-primary"
        style={{ marginRight: "10px" }}
        onClick={() => getRequestWithFetch("users")}
      >
        GET Request (Users)
      </button>
      <button
        className="btn btn-primary"
        style={{ marginRight: "10px" }}
        onClick={() => getRequestWithFetch("photos")}
      >
        GET Request (Photos)
      </button>
      <button className="btn btn-primary" onClick={postRequestWithFetch}>
        POST Request
      </button>
      <pre style={{ background: "#eee", marginTop: "20px", padding: "20px" }}>
        <code>{data}</code>
      </pre>
    </div>
  );
};

export default Index;

En Next.js, las páginas (que se convierten automáticamente en rutas o URL) se definen mediante componentes de React.js. Aquí, estamos utilizando el enfoque basado en funciones para definir un componente en React que consiste en una función de JavaScript simple que devuelve algo de marcado JSX (el lenguaje de marcado creado para crear componentes en React).

En el cuerpo de esa función, también podemos definir otras funciones y hacer llamadas a un tipo especial de función exclusivo de React llamado ganchos.

Comenzando justo dentro del cuerpo de nuestra función, podemos ver una llamada a una de estas funciones gancho useState() (importado arriba) que nos permitirá establecer un valor de estado dinámico y luego acceder a ese valor en nuestro marcado JSX y las otras funciones definidas dentro del cuerpo de nuestro componente de función (un concepto conocido como "funciones de cierre" o funciones definidas dentro de funciones en JavaScript).

Aquí, useState([]) está diciendo "crear una instancia de un valor de estado, establecer el valor predeterminado en una matriz vacía [] ."

Para el valor de retorno de esa llamada, esperamos obtener una matriz con dos valores:el primero es el valor actual data y la segunda es una función que podemos usar para actualizar ese valor setData . Aquí, usamos la desestructuración de matrices de JavaScript para acceder al contenido de nuestra matriz y, al mismo tiempo, asignamos variables a los valores en esas posiciones de la matriz.

Para aclarar eso, si escribimos esta línea como const state = useState([]) , tendríamos que seguir esa línea con algo como:

const data = state[0];
const setData = state[1];

Usando la desestructuración de matrices, podemos evitar esto por completo.

Saltando más allá de nuestras funciones de marcador de posición, a continuación, mirando el marcado JSX que estamos devolviendo desde nuestro Index función del componente (lo que Next.js representará para nuestra página), podemos ver que nuestra interfaz de usuario real es bastante simple:representamos tres botones y un <pre></pre> bloquear.

La idea aquí es que tenemos un botón para cada uno de nuestros fetch() tipos de solicitudes, seguido de un bloque de código en el que representamos la respuesta a cada solicitud (activada por el clic del botón). Aquí podemos ver el data variable que "arrancamos" usando la desestructuración de la matriz de nuestra llamada a useState() siendo pasado al <code></code> etiqueta anidada dentro de nuestro <pre></pre> etiqueta. Aquí es donde finalmente almacenaremos los datos de respuesta de nuestro fetch() solicitudes (y ver esos datos en pantalla).

Mirando cada botón, podemos ver el onClick atributo al que se le asigna un valor. Para los dos primeros botones, de los que seremos responsables, realice nuestro GET solicitar ejemplos:llamamos a la función definida anteriormente getRequestWithFetch() , pasando una cadena que describe el recurso o la ruta a la que nos gustaría llamar (esto tendrá más sentido en un momento).

Para el último botón, simplemente pasamos la función postRequestWithFetch directamente ya que no necesitamos pasar ningún argumento cuando llamamos a esa función.

/cliente/páginas/index.js

import React, { useState } from "react";

const Index = () => {
  const [data, setData] = useState([]);

  const getRequestWithFetch = (resource = "") => {
    fetch(`http://localhost:5001/${resource}`, {
      credentials: "include",
    }).then(async (response) => {
      const data = await response.json();

      // NOTE: Doing JSON.stringify here for presentation below. This is not required.
      setData(JSON.stringify(data, null, 2));
    });
  };

  const postRequestWithFetch = () => {
    // We'll make a our POST request using fetch here...
  };

  return (
    <div>
      ...
    </div>
  );
};

export default Index;

Mirando el getRequestWithFetch función que insinuamos a continuación, podemos ver que la cadena que pasamos para nuestro nombre de recurso se define como el argumento resource en nuestra función. Dentro de esa función, configuramos nuestra llamada a fetch() . Algo que notará es que, a diferencia del servidor, no estamos importando fetch() desde cualquier lugar.

Esto se debe a que fetch está integrado en los navegadores modernos como un global value (lo que significa que se define automáticamente en todas partes del navegador).

Mirando nuestra llamada, tal como vimos antes, llamamos a fetch() pasando una URL como primer argumento. En este caso, estamos pasando la URL de uno de los GET rutas que definimos en nuestro servidor anteriormente. Esto cambiará dinámicamente según el valor pasado para resource , a http://localhost:5001/users o http://localhost:5001/photos .

Como segundo argumento de fetch() , pasamos un objeto de opciones. Aquí, solo estamos pasando una sola propiedad credentials: "include" . Como veremos cuando implementemos nuestra solicitud POST, lo que pasemos aquí determina cómo se comporta realmente nuestra solicitud. En este caso, le decimos fetch() para incluir las cookies del navegador en los encabezados de solicitud cuando envía la solicitud. Aunque no estamos autenticando nuestras solicitudes en el servidor, es importante tener esto en cuenta si espera fetch() comportarse como un navegador (que envía automáticamente las cookies con sus propias solicitudes).

Finalmente, aquí, abajo en el .then() devolución de llamada (recuerde, fetch() nos devolverá una promesa de JavaScript), usamos el patrón async/await para await response.json() para recuperar los datos devueltos en un formato compatible con JavaScript (matriz u objeto) y luego llamar al setData() función que obtuvimos de nuestro useState() función de gancho para configurar los datos de respuesta para mostrarlos en nuestro <pre></pre> etiqueta.

/cliente/páginas/index.js

import React, { useState } from "react";

const Index = () => {
  const [data, setData] = useState([]);

  const getRequestWithFetch = (resource = "") => {
    ...
  };

  const postRequestWithFetch = () => {
    fetch(`http://localhost:5001/users`, {
      method: "POST",
      credentials: "include",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: "test",
      }),
    }).then(async (response) => {
      const data = await response.text();
      setData(data);
    });
  };

  return (
    <div>
      ...
    </div>
  );
};

export default Index;

A continuación, para nuestro postRequestWithFetch() función, repetimos un proceso similar a nuestra solicitud GET. Aquí, sin embargo, codificamos nuestra URL (solo tenemos una ruta POST en el servidor) y, debido a que estamos realizando una solicitud que no es GET, configuramos un method opción a POST . Si no haz esto, fetch() asumirá que estamos intentando realizar una solicitud GET o "obtener" algunos datos.

Debajo de esto, podemos ver el mismo credentials: "include" como nuestra solicitud GET (de nuevo, puramente para la concienciación aquí). A continuación, la parte importante, dado que se trata de una solicitud POST, agregamos un body opción establecida en un objeto JavaScript en cadena con algunos datos de prueba en él. Recuerde, las solicitudes HTTP solo pueden pasar cadenas de un lado a otro. Para que esto funcione, en el headers opción, agregamos el HTTP Content-Type encabezado, configurándolo en application/json . Esto es importante. Esto comunica al servidor que los datos que enviamos en el cuerpo deben analizarse como datos JSON.

/servidor/middleware/bodyParser.js

import bodyParser from "body-parser";

export default (req, res, next) => {
  const contentType = req.headers["content-type"];

  if (contentType && contentType === "application/x-www-form-urlencoded") {
    return bodyParser.urlencoded({ extended: true })(req, res, next);
  }

  return bodyParser.json()(req, res, next);
};

Para darle sentido a esto, rápidamente, en el lado del servidor de nuestra aplicación, el modelo de Node.js que estamos usando tiene algo conocido como middleware función que se ejecuta cada vez que ingresa una solicitud al servidor, justo antes de que se entregue a nuestras rutas Express.js. Aquí, podemos ver en la parte inferior de la función de middleware que analiza el cuerpo de la solicitud HTTP en un formato JSON.

Si no establece el Content-Type encabezado en nuestro fetch() solicitar de nuevo en el cliente, nuestro cuerpo de solicitud (req.body en nuestro controlador de ruta en el servidor) sería un objeto vacío. Sin embargo, una vez que configuramos este encabezado, el servidor que responde a nuestra solicitud sabe "qué hacer" y recibe el cuerpo de nuestra solicitud según lo previsto.

/cliente/páginas/index.js

import React, { useState } from "react";

const Index = () => {
  const [data, setData] = useState([]);

  const getRequestWithFetch = (resource = "") => {
    ...
  };

  const postRequestWithFetch = () => {
    fetch(`http://localhost:5001/users`, {
      method: "POST",
      credentials: "include",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: "test",
      }),
    }).then(async (response) => {
      const data = await response.text();
      setData(data);
    });
  };

  return (
    <div>
      ...
    </div>
  );
};

export default Index;

Volviendo a centrarnos en nuestro postRequestWithFetch función en el cliente, en el .then() devolución de llamada, usamos un flujo similar al que vimos antes con async/await, sin embargo, esta vez, en lugar de response.json() usamos response.text() . Esto se debe a que la respuesta que enviamos desde el servidor para nuestra solicitud POST es solo una cadena simple (a diferencia de un objeto en cadena como en nuestras otras solicitudes). Una vez que tengamos nuestro data , lo colocamos en el estado con setData() .

¡Eso es todo! Ahora estamos listos para probar esto:

Terminando

En este tutorial, aprendimos cómo realizar solicitudes HTTP usando JavaScript fetch() API. Comenzamos en el servidor, definiendo rutas para enviar nuestras solicitudes desde el cliente, también aprendiendo a usar fetch() a través del isomorphic-fetch biblioteca desde dentro de Node.js. Luego, en el cliente, aprendimos cómo ejecutar solicitudes HTTP GET y POST, aprendiendo sobre las opciones adecuadas para pasar para garantizar que nuestro servidor pueda entender nuestra solicitud.