Partiendo del post anterior en el que introduje el funcionamiento básico de GraphQL, vamos a ir un poco más allá y atacaremos a una API Rest para utilizar datos en vivo. El objetivo es ver cómo puede convivir GraphQL con tu backend Rest.

Consultas contra una API Rest

Vamos a pedir los "values" de las "keys" de una consulta GraphQL a una API Rest. Para esto tendremos que trabajar con promesas. En estos casos, suelo elegir la PokeApi. Comenzaremos modelando los datos con pseudolenguaje, para luego ver cómo se codifica en Javascript:

Antes de empezar, aquí está el código del cliente de la API que vamos a usar en los ejemplos:

import fetch from 'node-fetch';

const BASE_API = 'http://pokeapi.co';

export function pokemon({id = 1} = {}){  
  return fetch(`${BASE_API}/api/v1/pokemon/${id}`)
          .then((response) => response.json());
};

export async function descriptions({pokemon = false} = {}){  
  return await pokemon.descriptions.map(desc => {
    return fetch(`${BASE_API}${desc.resource_uri}`)
    .then(response => response.json());
  });
};

Y ahora vamos a definir nuestro primer objeto. Vamos a definir cómo sería un Pokémon.

 type Pokemon {
     name: String
    defense: Integer
    attack: Integer
    descriptions: [Description]
 }

El pseudocódigo indica que un Pokémon es un objeto que tiene 4 claves. Las 3 primeras son de tipo básico, pero la última, descriptions, es un listado de objetos de tipo Description.

Usando la librería de Facebook, se codifica así:

var pokemonType = new GraphQLObjectType({  
  name: 'Pokemon',
  description: 'A little Pokemon thing',
  fields: () => ({
    name: {
      type: GraphQLString,
      description: 'Pokemon's name.'
    },
    defense: {
      type: GraphQLInt,
      description: 'Defense value.'
    },
    attack: {
      type: GraphQLInt,
      description: 'Attack value.'
    },
    descriptions: {
      type: new GraphQLList(descriptionType),
      description: 'more detailed information',
      resolve: (resource) => descriptions({pokemon: resource})
    }
  })
});

La clave está en la key fields, porque asocia la respuesta de la API a las keys usadas en nuestra consulta GraphQL. Fíjate cómo hay una entrada por cada una de las keys que hemos definido, donde especificamos el tipo de valor esperado. La función resolve dentro del campo descriptions es especial porque asocia el valor de la key con otro recurso distinto de la API Rest. Esta función perfectamente puede ser asíncrona y devolver una promesa. Como de hecho hace.

Como hemos hecho referencia al objeto Description, debemos definirlo. Y para nosotros sería algo así:

 type Description {
    name: String
    id: String!
    created: String
    modified: String
    pokemon: Pokemon
 }

La codificación de ese nuevo tipo en JS sería algo así:

var descriptionType = new GraphQLObjectType({  
  name: 'Description',
  description: 'Details about a pokemon',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The id of the description.'
    },
    name: {
      type: GraphQLString,
      description: 'Name of the description.'
    },
    description: {
      type: GraphQLString,
      description: 'The description of the pokemon.'
    },
    modified: {
      type: GraphQLString,
      description: 'Last modified date.'
    },
    created: {
      type: GraphQLString,
      description: 'created date.'
    }
  })
});

Aquí definimos cada uno de los 5 campos del objeto Description.
Un detalle importante sobre todos estos campos es que no tienen función resolve. Eso significa que los campos van a coincidir directamente a la respuesta de la API. Lo cual es fenomenal para reducir la cantidad de código a escribir.

Lo siguiente que tenemos que hacer es definir un tipo especial de objeto un objeto query. Uno cuyos campos estan asociados a tipos definidos anteriormente. Y que todos tienen método resolver, que se van a encargar de llamar a la API para obtener los datos.

La definición sería algo así:

 type Query {
    pokemon(id: String): Pokemon
 }

Además no son claves de objetos normales, estos parecen métodos, porque reciben parámetros. Tiene sentido si necesitamos indicar un recurso en concreto. Lo que si podemos observar es que todas las claves devuelven un tipo definido por nosotros.

Esto en Javascript quedaría así:

var queryType = new GraphQLObjectType({  
  name: 'Query',
  fields: () => ({
    pokemon: {
      type: pokemonType,
      args: {id: { name: 'id', type: new GraphQLNonNull(GraphQLString)}},
      resolve: (root, {id}) => pokemon({id})
    }
  })
});

Fíjate que es idético a cualquier otra definición de tipo. Es una instancia de la misma clase y también tiene name y fields. Pero el campo pokemon tiene una nueva clave args que define los parámetros y el tipo que se le va a pasar a la función resolve
. Por otra parte la función resolve usa nuestro cliente de la API para devolver in objeto Pokemon.

Una vez que tenemos nuestro objeto query, y todos los tipos definidos, estamos preparados para crear nuestro esquema y exportarlo.

export var PokemonSchema = new GraphQLSchema({  
  query: queryType
});

Básicamente aquí estamos diciendo que podemos pedir "objetos JSON" que tengan las mismas claves que nuestro objeto queryType.

Por fin podemos hacer una consulta a nuestro esquema recien creado.

const query = `  
  query PokemonQuery($pokemonId: String!) {
    pokemon(id: $pokemonId) {
      name
      defense
      attack
      descriptions {
        description
      }
    }
  }
`;

graphql(PokemonSchema, query, null, {pokemonId: 7}).then(pokemon => console.log(pokemon));  

Esta consulta es super interesante. Estamos pidiendo completar la clave pokemon (definida en queryType) y luego "recursivamente" sobre el resultado de completar esa clave, completar la clave descriptions que a su vez pide completar la clave description.

Piensalo de forma recursiva, de arriba hacia abajo, vamos rellenando las claves pedidas en las consultas.

Lo increible de este nuevo sistema es que si el cliente no desea alguno de esos campos simplemente no tiene que pedirlos, pero que además no tiene ni idea de la estructura interna de los datos. Es decir no sabe si el recurso pokemon tiene un campo description dentro y este un campo name, o si description solo es un puntero a otro recurso. Eso al cliente no le importa, el solo pide los datos como desea consumirlos.

Para que te hagas una idea de la salida de consola con una query así, sería algo más o menos como esto:

Servidor GraphQL

Es genial poder lanzar queries contra nuestro esquema, pero esto se vuelve realmente interesante cuando lo ofrecemos como servicion, mediante un servidor web. Una vez que hemos llegado hasta aquí, es relativamente fácil, poner un server a la escucha en un puerto que reciba un JSON con la query y los parámetros y devuelva la respuesta dada por la función graphql.

import {PokemonSchema} from './pokemon-schema';  
import { graphql } from 'graphql';

import express from 'express';  
import bodyParser from 'body-parser'

const PORT = 8080;

const app = express();

app.use(bodyParser.json());

app.post('/', (req, res, next) => {  
  const {query, pokemonId} = req.body;
  graphql(PokemonSchema, query, null, {pokemonId}).then(res.json.bind(res));
});

app.listen(PORT, () => {  
  console.log(`Server up and running in port ${PORT}`);
});

Si ponemos el serve a la escucha en el puerto 8080 y lanzamos un cUrl como este:

curl -H "Content-Type: application/json" \  
     -X POST -d '{"query":"query PokemonQuery($pokemonId: String!) {pokemon(id: $pokemonId) {name\ndefense}}", "pokemonId": "7"}' \
     http://localhost:8080

Como la query no se ve bien en el comando cUrl te la pongo aquí abajo:

{
    "query":
        "query PokemonQuery($pokemonId: String!) {
            pokemon(id: $pokemonId) {
                name
                defense
            }
         }"

con esta llamada obtendremos como respuesta:

Que es exactamente lo que hemos pedido.

Conclusión

GraphQL acaba de nacer, y aun hay muchisimas cosas que quedan por llegar de esta nueva tecnología, pero personalmente la considero prometedora. Yo acabo de empezar a aprenderla, y estoy seguro que aun me quedan muchísimas cosas por descubrir de ella. Por lo pronto no hemos hablado de como alterar los datos en el servidor. Pero eso lo haremos pronto.

Personalmente creo que el potencial para tener una única fuente de datos y que los clientes puedan cambiar el "dibujo" del objeto de datos que necesita del backend sin que potencialmente haya que cambiar nada, me parece algo increible. Además el poder pedir los datos tal y como te gustaría consumirlo, sin tener en cuenta de la estructura de ellos en el backend también es algo con mucho potencial.

Te dejo aquí un repositorio con todo el código sobre el que me he basado para desarrollar estos dos últimos post. Esta vez, de verdad, te recomiendo que le eches un ojo y que jueges cambiandolo todo. Porque así es como realmente vas a tener ese momento eureka que es tan maravillo.

Suscríbete a mi lista de correo

* Campos obligatorios