Cómo configurar un servidor GraphQL con Apollo Server y Express

Cómo configurar y manejar correctamente las solicitudes a un servidor GraphQL utilizando la biblioteca del servidor Apollo junto con un servidor Express.js existente.

Primeros pasos

Para comenzar, nos basaremos en CheatCode Node.js Boilerplate. Esto nos dará un servidor GraphQL ya configurado para trabajar y agregar contexto a las explicaciones a continuación. Primero, clone el modelo a través de Github:

Terminal

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

A continuación, cd en el nodejs-server-boilerplate clonado directorio e instalar las dependencias:

Terminal

cd nodejs-server-boilerplate && npm install

A continuación, agreguemos manualmente el apollo-server dependencia (esto es diferente del apollo-server-express dependencia que ya está incluida en el modelo; veremos esto más adelante):

Terminal

npm i apollo-server

Una vez que esto esté completo, se instalarán todas las dependencias que necesita para el resto del tutorial. Ahora, para comenzar, echemos un vistazo a cómo configurar un servidor GraphQL básico con Apollo Server.

Configurando el servidor base

Para comenzar, necesitamos importar dos cosas como exportaciones con nombre de apollo-server , el ApolloServer constructor y el gql función.

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

// We'll set up our server here.

Para crear un servidor, a continuación, creamos una nueva instancia de ApolloServer con new ApolloServer() :

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  typeDefs: gql`
    type Example {
      message: String
    }

    type Query {
      queryExample: Example
    }

    type Mutation {
      mutationExample: Example
    }
  `,
  resolvers: {
    Query: {
      queryExample: (parent, args, context) => {
        return {
          message: "This is the message from the query resolver.",
        };
      },
    },
    Mutation: {
      mutationExample: (parent, args, context) => {
        console.log("Perform mutation here before responding.");

        return {
          message: "This is the message from the mutation resolver.",
        };
      },
    },
  },
});

Hemos agregado mucho aquí, así que vamos a revisarlo. Primero, creamos una variable server y configúrelo igual al valor de retorno de llamar a new ApolloServer() . Esta es nuestra instancia de Apollo Server. Como argumento a ese constructor para configurar nuestro servidor, le pasamos un objeto con tres propiedades:playground , typeDefs y resolvers .

Aquí, playground se le asigna un true booleano valor que le dice a Apollo Server que habilite la GUI de GraphQL Playground en /graphql cuando el servidor se está ejecutando. Esta es una herramienta útil para probar y depurar su API GraphQL sin tener que escribir un montón de código front-end. Por lo general, es bueno limitar el uso del área de juegos solo a su desarrollo NODE_ENV . Para hacerlo, puede establecer playground aquí para process.env.NODE_ENV === 'development' .

A continuación, el typeDefs y resolvers propiedades aquí, juntas, describen el esquema para su servidor GraphQL. El primero, typeDefs es la parte de su esquema donde define los posibles tipos, consultas y mutaciones que el servidor puede manejar. En GraphQL, hay dos raíz tipos Query y Mutation que se puede definir junto con sus tipos personalizados (que describen la forma de los datos devueltos por sus consultas y mutaciones) como type Pizza {} .

Arriba, hemos especificado un esquema de ejemplo completo. Primero, observe que hemos asignado nuestro typeDefs valor igual a gql`` donde gql() es una función que espera un solo argumento como una cadena. La sintaxis aquí (sin paréntesis después de gql ) es una característica integrada de JavaScript que le permite invocar una función y pasarle un valor de cadena al mismo tiempo. Para ser claros, lo anterior es equivalente a gql(´´) . El uso de esta sintaxis requiere que el valor de cadena pasado se haga como un literal de plantilla (es decir, una cadena definida usando acentos graves en lugar de comillas simples o dobles).

El gql´´ La función en sí es responsable de tomar una cadena que contiene código escrito en GraphQL DSL (lenguaje específico del dominio). DSL, aquí, se refiere a la sintaxis única del lenguaje GraphQL. A la hora de definir nuestro esquema, tenemos la opción de escribirlo en el DSL de GraphQL. El gql`` La función toma esa cadena y la convierte del DSL en un árbol de sintaxis abstracta (AST) que, como un objeto que describe el esquema en un formato que GraphQL puede entender.

Dentro de la cadena pasamos a gql() , primero, hemos incluido un tipo de datos como type Example que define un type personalizado (no el Query incorporado o Mutation tipos) que describe un objeto que contiene un message campo cuyo valor debe ser un String . A continuación, definimos la raíz Query escribe y Mutation escribe. En la raíz Query tipo, definimos un campo queryExample (que esperamos emparejar con una función de resolución a continuación) que esperamos devolver datos en la forma de type Example acabamos de definir. A continuación, hacemos lo mismo para nuestra raíz Mutation tipo, agregando mutationExample y también esperando un valor de retorno en forma de type Example .

Para que esto funcione, necesitamos implementar funciones de resolución en el resolvers objeto (pasado a nuestro ApolloServer constructor). Fíjate que aquí, dentro de resolvers hemos definido un Query propiedad y un Mutation propiedad. Estos imitan intencionalmente la estructura de type Query y type Mutation arriba. La idea aquí es que la función resolvers.Query.queryExample se llamará cada vez que se ejecute una consulta en el queryExample campo de un cliente (navegador o aplicación nativa), completando o resolviendo la consulta.

Exactamente lo mismo está ocurriendo en resolvers.Mutation.mutationExample , sin embargo, aquí estamos definiendo una mutación (es decir, esperamos que este código cambie algunos datos en nuestra fuente de datos, no solo devuelva algunos datos de nuestra fuente de datos). Observe que la forma del objeto devuelto tanto por el queryExample resolver y mutationExample resolver coincide con la forma del type Example definimos anteriormente. Esto se hace porque, en nuestra raíz Query y raíz Mutation , hemos especificado que el valor devuelto por esos resolutores tendrá la forma de type Example .

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  typeDefs: gql`...`,
  resolvers: { ... },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

export default () => {};

Finalmente, con nuestro typeDefs y resolvers definido, ponemos nuestro servidor en uso. Para hacerlo, tomamos el server variable en la que almacenamos nuestro servidor Apollo anteriormente y la llamamos listen() método que devuelve una Promesa de JavaScript (de ahí el .then() la sintaxis se encadena al final). Pasado a listen() , proporcionamos un objeto de opciones con una sola propiedad port igual a 3000 . Esto le indica a Apollo Server que escuche las conexiones entrantes en localhost:3000 .

Con esto, deberíamos tener un servidor Apollo en funcionamiento. Importante, porque estamos sobrescribiendo el /api/graphql/server.js incluido en el modelo de Node.js desde el que comenzamos, hemos agregado un export default () => {} , exportando una función vacía para cumplir con las expectativas del servidor Express.js existente (aprenderemos cómo conectar el servidor Apollo con este servidor Express más adelante en el tutorial).

Para probar esto, desde la raíz del repetitivo, ejecute npm run dev para iniciar el servidor. Advertencia justa, porque estamos iniciando dos servidores separados con este comando (el servidor Apollo que acabamos de implementar arriba y el servidor Express existente incluido en el modelo), verá dos declaraciones registradas que le indicarán que el servidor se está ejecutando en diferentes puertos:

Terminal

Server running at http://localhost:5001
Server running at http://localhost:3000/

Antes de pasar a combinar este nuevo servidor Apollo con el servidor Express existente en el modelo estándar, veamos cómo configurar un contexto personalizado para los resolutores.

Configuración del contexto de resolución

Si bien técnicamente tenemos un servidor GraphQL en funcionamiento en este momento (puede verificar esto visitando http://localhost:3000/graphql en su navegador), es bueno saber cómo configurar un contexto de resolución personalizado, ya que esto influye en la autenticación del usuario cuando usa GraphQL como su capa de datos principal.

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  context: async ({ req, res }) => {
    const token = req?.cookies["jwt_token"];

    const context = {
      req,
      res,
      user: {},
    };

    const user = token ? await authenticationMethod({ token }) : null;

    if (!user?.error) {
      context.user = user;
    }

    return context;
  },
  typeDefs: gql`...`,
  resolvers: { ... },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

export default () => {};

En GraphQL, ya sea que esté realizando una consulta o una mutación, sus funciones de resolución reciben un context objeto como argumento final. Este objeto contiene el "contexto" actual para la solicitud que se realiza al servidor GraphQL. Por ejemplo, si un usuario inició sesión en su aplicación y realiza una solicitud de GraphQL, es posible que deseemos incluir la información de la cuenta del usuario en el contexto para ayudarnos a resolver la consulta o la mutación (p. permisos para acceder a esa consulta o mutación).

Aquí, junto al playground , typeDefs y resolvers propiedades que agregamos anteriormente, agregamos context ajustado a una función. Apollo Server llama automáticamente a esta función cada vez que llega una solicitud al servidor. Se pasa un objeto de opciones como argumento que contiene la solicitud del servidor req y respuesta res objetos (lo que Apollo Server usa internamente para responder a la solicitud HTTP realizada al servidor GraphQL).

Desde esa función, queremos devolver un objeto que represente el context argumento que queremos que esté disponible en todos nuestros resolutores. Arriba, presentamos un ejemplo hipotético en el que anticipamos que se pasa una cookie HTTP al servidor (junto con la solicitud de GraphQL) y la usamos para autenticar a un usuario. Nota :esto es pseudocódigo y no devolver un usuario en su estado actual.

Para asignar el usuario al objeto de contexto, definimos una base context objeto primero, que contiene el req y res del objeto de opciones pasado a la función de contexto a través de Apollo Server y combínelo con un objeto vacío que representa a nuestro usuario. A continuación, intentamos autenticar a nuestro usuario usando el supuesto jwt_token Galleta. Nuevamente, hipotéticamente, si esta función existiera, esperaríamos que devolviéramos un objeto de usuario (por ejemplo, que contenga una dirección de correo electrónico, un nombre de usuario y otros datos de identificación del usuario).

Finalmente, desde el context: () => {} función, devolvemos el context objeto definimos (con el req , res y user ) valores.

/api/graphql/server.js

import * as apolloServer from "apollo-server";
const { ApolloServer, gql } = apolloServer.default;

const server = new ApolloServer({
  playground: true,
  context: async ({ req, res }) => {
    [...]

    return context;
  },
  typeDefs: gql`...`,
  resolvers: {
    Query: {
      queryExample: (parent, args, context) => {
        console.log(context.user);
        return {
          message: "This is the message from the query resolver.",
        };
      },
    },
    Mutation: {
      mutationExample: (parent, args, context) => {
        console.log(context.user);
        console.log("Perform mutation here before responding.");

        return {
          message: "This is the message from the mutation resolver.",
        };
      },
    },
  },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

Mostrando cómo utilizar el contexto, aquí, dentro de nuestro queryExample y mutationExample resolutores, hemos cerrado la sesión del context.user valor que establecimos arriba.

Adjuntar el servidor GraphQL a un servidor Express existente

Hasta este punto, hemos estado configurando nuestro servidor Apollo para que sea un independiente Servidor GraphQL (es decir, no lo adjuntamos a un servidor existente). Aunque esto funciona, limita nuestro servidor a tener solo un /graphql punto final Para evitar esto, tenemos la opción de "conectar" nuestro servidor Apollo a un servidor HTTP existente.

Lo que vamos a hacer ahora es volver a pegar en la fuente original del /api/graphql/server.js archivo que sobrescribimos anteriormente con nuestro servidor GraphQL independiente:

/api/graphql/server.js

import { ApolloServer } from "apollo-server-express";
import schema from "./schema";
import { isDevelopment } from "../../.app/environment";
import loginWithToken from "../users/token";
import { configuration as corsConfiguration } from "../../middleware/cors";

export default (app) => {
  const server = new ApolloServer({
    ...schema,
    introspection: isDevelopment,
    playground: isDevelopment,
    context: async ({ req, res }) => {
      const token = req?.cookies["app_login_token"];

      const context = {
        req,
        res,
        user: {},
      };

      const user = token ? await loginWithToken({ token }) : null;

      if (!user?.error) {
        context.user = user;
      }

      return context;
    },
  });

  server.applyMiddleware({
    cors: corsConfiguration,
    app,
    path: "/api/graphql",
  });
};

Algo de esto debería parecer familiar. Primero, observe que en lugar de llamar a new ApolloServer() directamente dentro del cuerpo de nuestro /api/graphql/server.js archivo, hemos envuelto esa llamada en una función que espera app como argumento. Aquí, app representa el servidor Express.js existente configurado en /index.js en el modelo estándar de Node.js que hemos estado usando a lo largo de este tutorial.

Dentro de la función (observe que estamos exportando esta función como la exportación predeterminada para el archivo), configuramos nuestro servidor Apollo tal como lo hicimos anteriormente. Aquí, sin embargo, observe que typeDefs y resolvers faltan como propiedades. Estos están contenidos dentro del schema valor importado del ./schema.js archivo en el mismo directorio en /api/graphql/schema.js .

El contenido de este archivo es casi idéntico al que vimos anteriormente. Está separado en el texto estándar con fines organizativos; esto no servir a cualquier propósito técnico. Para utilizar ese archivo, usamos el operador de propagación de JavaScript ... para decir "desempaquetar el contenido del objeto contenido en el schema importado valor en el objeto que estamos pasando a new ApolloServer() ." Como parte de este desempaquetado, el typeDefs y resolvers propiedades en ese importado el objeto se volverá a asignar a las opciones que estamos pasando a new ApolloServer() .

Justo debajo de esto, también podemos ver que se agrega una nueva propiedad introspection . Esto, junto con el playground existente propiedad que vimos antes:se establece en el valor de isDevelopment , un valor que se importa mediante el .app/environment.js desde la raíz del proyecto y nos dice si nuestro process.env.NODE_ENV el valor es igual a development (lo que significa que estamos ejecutando este código en nuestro entorno de desarrollo).

El introspection La propiedad le dice a Apollo Server si debe permitir o no que los clientes GraphQL "introspeccionen" o descubran los tipos, consultas, mutaciones, etc. que ofrece el servidor GraphQL. Si bien esto es útil para la depuración y las API públicas creadas con GraphQL, es un riesgo de seguridad para las API privadas creadas con GraphQL.

/api/graphql/server.js

import { ApolloServer } from "apollo-server-express";
import schema from "./schema";
import { isDevelopment } from "../../.app/environment";
import loginWithToken from "../users/token";
import { configuration as corsConfiguration } from "../../middleware/cors";

export default (app) => {
  const server = new ApolloServer({ [...] });

  server.applyMiddleware({
    cors: corsConfiguration,
    app,
    path: "/api/graphql",
  });
};

Con todo ese conjunto, finalmente, la parte que conecta nuestro servidor Apollo a nuestro servidor Express.js existente es el server.applyMiddleware() en la parte inferior de nuestra función exportada. Esto toma en tres propiedades:

  • cors que describe la configuración y los permisos de CORS para qué los dominios pueden acceder al servidor GraphQL.
  • app que representa nuestro existente Servidor Express.js.
  • path que describe en qué URL en nuestro existente servidor Express.js, se podrá acceder al servidor GraphQL.

Para el cors propiedad, utilizamos el middleware CORS que se incluye con el modelo de Node.js que estamos usando (veremos esto en detalle en la siguiente sección). Para el path , especificamos que nuestro servidor GraphQL se adjuntará a nuestro servidor en ejecución (iniciado en el puerto 5001 ejecutando npm run dev desde la raíz del proyecto) en la ruta /api/graphql . En otras palabras, en lugar del http://localhost:3000/graphql ruta que vimos anteriormente, ahora estamos "aprovechando" el servidor Express.js existente y haciendo que nuestro servidor GraphQL sea accesible en eso puerto del servidor (5001) en http://localhost:5001/api/graphql .

El resultado final es efectivamente el mismo:obtenemos un servidor GraphQL en ejecución a través del servidor Apollo, pero no active otro servidor HTTP en un nuevo puerto.

Manejo de problemas de CORS al conectarse a través de clientes externos

Finalmente, un último detalle que debemos cubrir es la configuración de CORS. Como vimos en la sección anterior, confiamos en el cors middleware incluido en el modelo de Node.js que hemos usado a lo largo de este tutorial. Abramos ese archivo en el repetitivo y expliquemos cómo afecta a nuestro servidor GraphQL:

/middleware/cors.js

import cors from "cors";
import settings from "../lib/settings";

const urlsAllowedToAccess =
  Object.entries(settings.urls || {}).map(([key, value]) => value) || [];

export const configuration = {
  credentials: true,
  origin: function (origin, callback) {
    if (!origin || urlsAllowedToAccess.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`${origin} not permitted by CORS policy.`));
    }
  },
};

export default (req, res, next) => {
  return cors(configuration)(req, res, next);
};

Esto parece más amenazante de lo que es. Para ir al grano, el objetivo final aquí es decirle a la verificación CORS del navegador (CORS significa uso compartido de recursos de origen cruzado y define qué URL pueden acceder a un servidor) si la URL se realiza o no desde la solicitud (por ejemplo, una aplicación estamos corriendo en http://myapp.com ) puede acceder a nuestro servidor GraphQL.

configuración-desarrollo.json

{
  [...]
  "urls": {
    "api": "http://localhost:5001",
    "app": "http://localhost:5000"
  }
}

El acceso de esa solicitud se controla a través del urls lista incluida en el settings-<env>.json archivo en la raíz del proyecto. Esa configuración contiene una matriz de URL que pueden acceder al servidor. En este ejemplo, queremos que las mismas URL accedan a nuestro servidor Express.js existente para acceder a nuestro servidor GraphQL.

Aquí, http://localhost:5001 es el propio servidor (lo que significa que puede realizar solicitudes a sí mismo, si es necesario) y http://localhost:5000 es nuestra aplicación frontal orientada al cliente (usamos localhost:5000 porque ese es el puerto predeterminado en el que se ejecuta Next.js Boilerplate de CheatCode).

Terminando

En este tutorial, aprendimos cómo configurar un servidor GraphQL usando el apollo-server paquete utilizando dos métodos:definir un servidor como independiente Servidor GraphQL y conexión de un servidor GraphQL a un servidor existente Servidor HTTP (en este caso, un servidor Express.js).

También aprendimos cómo configurar un esquema GraphQL básico y adjuntar eso a nuestro servidor, así como también cómo definir un contexto personalizado para que nuestros resolutores manejen cosas como la autenticación desde nuestro servidor GraphQL.

Finalmente, echamos un vistazo a la configuración de CORS y entendimos cómo controlar el acceso a nuestro servidor GraphQL cuando lo conectamos a un servidor existente.