Node - GraphQL

Implémentation d'une API REST/GraphQL/MongoDB

Depuis quelques temps déjà, au gré de mes rencontres avec certains consultants et de mes lectures de certains blogs, j’entends parler de GraphQL, une autre façon de faire que REST pour implémenter une API web service. Dans cet article, l’idée n’est pas tellement de déterminer les conditions dans lesquelles utiliser cette techno plutôt qu’une autre mais plutôt de manipuler du code pour se faire la main sur le sujet avec pour fil conducteur des methodes REST que l’on va essayer de “traduire” en GraphQL pour l’esprit du jeu.

Code source de l’article

Tout le code de l’article se trouve sur GitLab.

Préparation

Outre

nous utiliserons également

Installation Node.js

Pour Windows 10.
Récupérer l’installateur de la version LTS courante (v8.11.1).
Installer avec l’option add to PATH (node, npm et PATH).
Tester dans la console comme suit:

C:\>node --version
v8.11.1

C:\>npm --version
5.6.0


   ╭─────────────────────────────────────╮
   │                                     │
   │   Update available 5.6.0 → 5.8.0    │
   │       Run npm i npm to update       │
   │                                     │
   ╰─────────────────────────────────────╯


C:\>

Si on souhaite mettre à jour npm comme indiqué:

C:\>npm i npm

Configuration Proxy Corporate

Si besoin, donc si présence d’un proxy corporate, configurer npm pour qu’il passe par celui-ci:

C:\>npm config set proxy http://user:pwd@proxy-url:proxy-port/
C:\>npm config set https-proxy http://user:pwd@proxy-url:proxy-port/

Installation MongoDB

Pour Windows 10.
Récupérer l’installateur de la version courante (v3.6.3).
Installer avec l’option complete.
Avec regedit ajouter C:\Program Files\MongoDB\Server\3.6\bin au Path (HKEY_CURRENT_USER).
Relancer une nouvelle session pour prise en compte du Path.
Tester dans la console comme suit:

C:\>mongod --version
db version v3.6.3
Rappels MongoDB

Permet la gestion de documents (données, lignes par analogie aux BDD relationnelles) dans des collections (tables par analogie aux BDD relationnelles).
Il n’y a pas de relations a priori (clés par analogie aux BDD relationnelles) entre les documents (NoSQL).
Les deux principaux binaires installés sont:

  • mongod: le serveur de base de données
  • mongo: shell mongodb

mongod démarre sur le port 27017 par défaut.
Avant de démarrer le serveur mongod il faut créer le répertoire des data utilisé par défaut C:\data\db ou utiliser le paramètre --dbpath pour spécifier un autre répertoire.

Création du projet

Initialisation node.js / npm

On crée un répertoire pour notre projet C:\node-rest-graphql-hello.
Initialiser un projet node.js dans ce répertoire (création d’un fichier package.json):

C:\node-rest-graphql-hello>npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (rest-graphql-hello) node-rgh
version: (1.0.0)
description: Demonstration of a node rest and graphql api
entry point: (index.js) server.js
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to C:\node-rest-graphql-hello>\package.json:

{
  "name": "node-rgh",
  "version": "1.0.0",
  "description": "Simple REST and GraphQL API engined by a Node.js/MongoDB backend",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes)

Initialisation Git

Créer le référentiel Git distant (sur GitLab par exemple) : node-rest-graphql-hello.

Intialisation

Avec Git BASH ou une invit de commande

C:\node-rest-graphql-hello>git init
Utilisateur

Avec Git BASH Pour une configuration locale au projet

$ git config --local user.name "your name"
$ git config --local user.email "your.email@yourwebmail.com"
Référentiel distant
$ git remote add origin git@gitlab.com:your-gitlab-user/your-gitlab-project.git
Commit et push
  • Commit local

    $ git add .
    $ git commit -m "init commit"
    
  • Push distant

    $ git push -u origin master
    

Nous pouvons désormais modifier notre code en local et pusher sur notre référentiel distant.

Importation des modules npm nécessaires

On se positionne dans notre répertoire projet C:\node-rest-graphql-hello.

Express (serveur HTTP et routes)

Invit de commande

C:\node-rest-graphql-hello>npm install express --save

Mongoose (accès MongoDB)

Invit de commande

C:\node-rest-graphql-hello>npm install mongoose --save

GraphQL

Invit de commande

C:\node-rest-graphql-hello>npm install express-graphql graphql --save

Contenu du fichier package.json

A ce stade, on doit avoir un fichier package.json ressemblant au suivant

{
  "name": "node-rgh",
  "version": "1.0.0",
  "description": "Simple REST and GraphQL API engined by a Node.js/MongoDB backend",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.3",
    "express-graphql": "^0.6.12",
    "graphql": "^0.13.2",
    "mongoose": "^5.0.13",
    "npm": "^5.8.0"
  }
}

Implémentation

On se positionne dans notre répertoire projet C:\node-rest-graphql-hello.

Démarrage d’un serveur HTTP

Créer

Créer le fichier server.js suivant

'use strict';

/* npm module express */
const express = require('express');
const app = express();

/* let's start ! */
var proxyPort = 3000;
app.listen(proxyPort, function () {
  console.log('API REST and GrapQL server is listening on port:', proxyPort);
});

Tester

Invit de commande

C:\node-rest-graphql-hello>npm start

> node-rgh@1.0.0 start C:\node-rest-graphql-hello
> node server.js

API REST and GrapQL server is listening on port: 3000

Première méthode REST API

Modifier

Modifier le fichier server.js comme suit

'use strict';

/* npm module express */
const express = require('express');
const app = express();

/* our first REST API method */
app.get('/', function(req, res) {
    res.status(200).json({"message" : "this is our first REST API method, server is running"});
    return;
  });  

/* let's start ! */
var proxyPort = 3000;
app.listen(proxyPort, function () {
  console.log('API REST and GrapQL server is listening on port:', proxyPort);
});

Tester

Le navigateur affiche le message suivant

{"message":"this is our first REST API method, server is running"}

Connexion au serveur mongodb

Modifier

Modifier le fichier server.js comme suit

'use strict';

/* settings */
const proxyPort = 3000;
const mongooseURL = 'mongodb://localhost:27017/nrghDB';

/* npm modules */
const express = require('express');
const mongoose = require('mongoose');

const app = express();

/* our first REST API method */
app.get('/', function (req, res) {
  res.status(200).json({ 'message': 'this is our first REST API method, server is running' });
  return;
});

/* let's start ! */
// == 1 ==
mongoose.connect(mongooseURL, function (error) {
  if (error) {
    console.log('FAILED : Unable to connect to MongoDB [%s]', mongooseURL);
    console.log('ABORTED : API REST and GrapQL server not started');
    process.exit(0);
  }
  else {
    console.log('SUCCEED : Connected to MongoDB [%s]', mongooseURL);

    // == 2 == 
    app.listen(proxyPort, function () {
      console.log('SUCCEED : API REST and GrapQL server started on port [%d]', proxyPort);
    });
  }
});

Démarrer le serveur mongo

Invit de commande

C:\node-rest-graphql-hello>mongod
Tester

Invit de commande

C:\node-rest-graphql-hello>npm start

> node-rgh@1.0.0 start C:\node-rest-graphql-hello
> node server.js

SUCCEED : Connected to MongoDB [mongodb://localhost:27017/nrghDB]
SUCCEED : API REST and GrapQL server started on port [3000]

Première requête GraphQL

Dans la suite on s’appuye sur le language de description GraphQL pour définir notre schéma et nos endpoints.
Il est également possible de définir ces éléments en les codant.

Modifier

Pour plus de clarté, on ajoute deux routeurs express, un pour REST et un pour GraphQL. On crée le schéma GraphQL avec une simple query hello ainsi que le resolver de cette query (fonction javascript du nom de la query contenue dans root qui implémente cette query).
Modifier le fichier server.js comme suit

'use strict';

/* settings */
const proxyPort = 3000;
const mongooseURL = 'mongodb://localhost:27017/nrghDB';

/* npm modules */
const express = require('express');
const mongoose = require('mongoose');
const graphqlHTTP = require('express-graphql');
const graphql = require('graphql');

const app = express();

/* we add two api routers */
var restApiRouter = express.Router();
var graphqlApiRouter = express.Router();
app.use('/api/rest', restApiRouter);
app.use('/api/graphql', graphqlApiRouter);

/* our first REST API method */
restApiRouter.get('/', function (req, res) {
  res.status(200).json({ 'hello': 'this is our first REST API method, server is running' });
  return;
});

/* our first GraphQL API query */
// == 1 == create GraphQL schema
let schema = graphql.buildSchema(`
type Query {
  hello: String
  }
  `);
// == 2 == create our "hello" resolver
var root = {
  hello: () => { return 'this is our first GraphQL API method, server is running'; }
};

graphqlApiRouter.use('/', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

/* let's start ! */
// == 1 ==
mongoose.connect(mongooseURL, function (error) {
  if (error) {
    console.log('FAILED : Unable to connect to MongoDB [%s]', mongooseURL);
    console.log('ABORTED : API REST and GraphQL server not started');
    process.exit(0);
  }
  else {
    console.log('SUCCEED : Connected to MongoDB [%s]', mongooseURL);

    // == 2 == 
    app.listen(proxyPort, function () {
      console.log('SUCCEED : API REST and GrapQL server started on port [%d]', proxyPort);
    });
  }
});

Redémarrer le serveur

Invit de commande

C:\node-rest-graphql-hello>npm start

> node-rgh@1.0.0 start C:\node-rest-graphql-hello
> node server.js

SUCCEED : Connected to MongoDB [mongodb://localhost:27017/nrghDB]
SUCCEED : API REST and GrapQL server started on port [3000]
Tester
  • Dans le navigateur taper http://localhost:3000/api/graphql?query={hello} (possible aussi en POST) Le message suivant s’affiche

    {"data":{"hello":"this is our first GraphQL API method, server is running"}}
    
  • ou taper http://localhost:3000/api/graphql et entrer la requête json suivante {hello}, comme graphiql=true l’application de test s’affiche (simili POSTMAN) dans le navigateur comme suit

LECTURE d’un document mongo en REST (GET) et GraphQL (Query)

Nous allons ajouter la possibilité de récupérer un ou des produits existants.
Pour cela nous allons créer

  • une méthode REST de type GET
  • une méthode GraphQL de type Query
Modifier le code

Modifier le fichier server.js comme suit

'use strict';

/* settings */
const proxyPort = 3000;
const mongooseURL = 'mongodb://localhost:27017/nrghDB';

/* npm modules */
const express = require('express');
const mongoose = require('mongoose');
const graphqlHTTP = require('express-graphql');
const graphql = require('graphql');

const app = express();

/* we add two api routers */
var restApiRouter = express.Router();
var graphqlApiRouter = express.Router();
app.use('/api/rest', restApiRouter);
app.use('/api/graphql', graphqlApiRouter);

/* create a mongoose schema */
var productSchema = mongoose.Schema({
  name: String,
  price: Number,
  desc: String
});

/* create a mongoose model */
var productModel = mongoose.model('Product', productSchema);

/* remove function : returns a promise */
var removeAllProducts = function () {
  console.log('removing all products');
  return productModel.remove(); // promise (see mongoose doc)
};

/* save function : returns a promise */
var saveProduct = function (pProduct) {
  console.log('saving new product');
  let newProduct = new productModel();
  newProduct.name = pProduct.name;
  newProduct.price = pProduct.price;
  newProduct.desc = pProduct.desc;
  return newProduct.save(); // promise (see mongoose doc)
};

/* find function : returns a promise */
var listProducts = function (query) {
  console.log('listing products', query);
  let execQuery = query ? query : {};    
  return productModel.find(execQuery, { '_id': 0, '__v': 0 }).exec(); // promise (see mongoose doc)
};

/* our first REST API method */
restApiRouter.get('/', function (req, res) {
  res.status(200).json({ 'message': 'this is our first REST API method, server is running' });
  return;
});

/* get products list : REST API method */
restApiRouter.get('/products', async function (req, res) {
  let products = [];
  try {
    products = await listProducts();
    res.status(200).json(products);
  } catch (err) {
    res.status(500).json(err); // displays error content : dangerous for security reason : only for demo purpose
  }
  return;
});

/* our GraphQL API */
// == 1 == create GraphQL schema
let schema = graphql.buildSchema(`
type Product {
  name: String!
  price: Int
  desc: String
}

type Query {
  message: String,
  products(name: String): [Product]
  }
  `);
// == 2 == our resolvers
var root = {
  message: () => { return 'this is our first GraphQL API method, server is running'; },  
  products: async ({name}) => {
    return await listProducts(name ? {'name': name} : {});
  }
};

graphqlApiRouter.use('/', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

/* let's start ! */
// == 1 ==
mongoose.connect(mongooseURL, function (error) {
  if (error) {
    console.log('FAILED : Unable to connect to MongoDB [%s]', mongooseURL);
    console.log('ABORTED : API REST and GraphQL server not started');
    process.exit(0);
  }
  else {
    console.log('SUCCEED : Connected to MongoDB [%s]', mongooseURL);

    // == 2 == 
    app.listen(proxyPort, async function () {
      console.log('SUCCEED : API REST and GrapQL server started on port [%d]', proxyPort);

      // == 3 ==
      try { await removeAllProducts(); }
      catch (err) { console.log(err); }

      try { await saveProduct({ 'name': 'desktop', 'price': 1000, 'desc': 'gamer desktop' }); }
      catch (err) { console.log(err); }
      try { await saveProduct({ 'name': 'laptop', 'price': 1500, 'desc': 'gamer laptop' }); }
      catch (err) { console.log(err); }
    });
  }
});

Redémarrer le serveur

Invit de commande

C:\node-rest-graphql-hello>npm start

> node-rgh@1.0.0 start C:\node-rest-graphql-hello
> node server.js

SUCCEED : Connected to MongoDB [mongodb://localhost:27017/nrghDB]
SUCCEED : API REST and GrapQL server started on port [3000]
removing all products
saving new product
saving new product
Tester REST

Dans le navigateur taper http://localhost:3000/api/rest/products
Le contenu de la collection mongo s’affiche (tableau json de 2 documents)

[
  {"name":"desktop","price":1000,"desc":"gamer desktop"},
  {"name":"laptop","price":1500,"desc":"gamer laptop"}
]
Tester GraphQL

Dans le navigateur taper http://localhost:3000/api/graphql

  • récupération de tous les documents

  • récupération d’un seul document

ECRITURE d’un document mongo en REST (POST) et GraphQL (Mutation)

Nous allons ajouter la possibilité de créer un nouveau produit.
Pour cela et toujours pour illustrer notre propos, nous allons créer

  • une méthode REST de type POST (par opposition au type GET pour la lecture)
  • une méthode GraphQL de type Mutation (par opposition au type Query pour la lecture)
Modifier le code

Modifier le fichier server.js comme suit

'use strict';

/* settings */
const proxyPort = 3000;
const mongooseURL = 'mongodb://localhost:27017/nrghDB';

/* npm modules */
const express = require('express');
const mongoose = require('mongoose');
const graphqlHTTP = require('express-graphql');
const graphql = require('graphql');

const app = express();

/* to support JSON-encoded and URL-encoded bodies */
app.use(express.json());
app.use(express.urlencoded({'extended': true}));

/* we add two api routers */
var restApiRouter = express.Router();
var graphqlApiRouter = express.Router();
app.use('/api/rest', restApiRouter);
app.use('/api/graphql', graphqlApiRouter);

/* ============================== */
/* ==== MONGOOSE DEFINITIONS ==== */
/* ============================== */
/* create a mongoose schema */
var productSchema = mongoose.Schema({
  name: String,
  price: Number,
  desc: String
});

/* create a mongoose model */
var productModel = mongoose.model('Product', productSchema);

/* remove function : returns a promise */
var removeAllProducts = function () {
  console.log('removing all products');
  return productModel.remove(); // promise (see mongoose doc)
};

/* save function : returns a promise */
var saveProduct = function (pProduct) {
  console.log('saving new product', JSON.stringify(pProduct));
  let newProduct = new productModel();
  newProduct.name = pProduct.name;
  newProduct.price = pProduct.price;
  newProduct.desc = pProduct.desc;  
  return newProduct.save(); // promise (see mongoose doc)
};

/* find function : returns a promise */
var listProducts = function (pQuery) {
  console.log('listing products');
  let query = pQuery ? pQuery : {};
  let projection = { '_id': 0, '__v': 0 };
  return productModel.find(query, projection).exec(); // promise (see mongoose doc)
};

/* ============================== */
/* ==== API REST DEFINITIONS ==== */
/* ============================== */
/* our first REST API method */
restApiRouter.get('/', function (req, res) {
  res.status(200).json({ 'message': 'this is our first REST API method, server is running' });
  return;
});

/* get products list : REST API method */
restApiRouter.get('/products', async function (req, res) {
  let products = [];
  try {
    products = await listProducts();
    res.status(200).json(products);
  } catch (err) {
    res.status(500).json(err); // displays error content : dangerous for security reason : only for demo purpose
  }
  return;
});

/* create a product : REST API method */
restApiRouter.post('/products', async function (req, res) {
  try {
    let product = await saveProduct(req.body);
    res.status(200).json(product);
  } catch (err) {
    res.status(500).json(err); // displays error content : dangerous for security reason : only for demo purpose
  }
  return;
});

/* ============================== */
/* == API GRAPHQL DEFINITIONS  == */
/* ============================== */
/* our GraphQL API */
// == 1 == create GraphQL schema
let schema = graphql.buildSchema(`
type Product {
  name: String!
  price: Int
  desc: String
}

input ProductInput {
  name: String!
  price: Int
  desc: String
}

type Query {
  message: String,
  products(name: String): [Product]
}

type Mutation  {
  saveProduct(input: ProductInput): Product
}  
`);

// == 2 == our resolvers
var root = {
  message: () => { return 'this is our first GraphQL API method, server is running'; },
  products: async ({ name }) => {
    return await listProducts(name ? { 'name': name } : {});
  },
  saveProduct: async ({ input }) => {
    return await saveProduct(input);
  },
};

graphqlApiRouter.use('/', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

/* ============================== */
/* ====== STARTING WEB PROXY ==== */
/* ============================== */
/* let's start ! */
// == 1 ==
mongoose.connect(mongooseURL, function (error) {
  if (error) {
    console.log('FAILED : Unable to connect to MongoDB [%s]', mongooseURL);
    console.log('ABORTED : API REST and GraphQL server not started');
    process.exit(0);
  }
  else {
    console.log('SUCCEED : Connected to MongoDB [%s]', mongooseURL);

    // == 2 == 
    app.listen(proxyPort, async function () {
      console.log('SUCCEED : API REST and GrapQL server started on port [%d]', proxyPort);

      // == 3 ==
      try { await removeAllProducts(); }
      catch (err) { console.log(err); }

      try { await saveProduct({ 'name': 'desktop', 'price': 1000, 'desc': 'gamer desktop' }); }
      catch (err) { console.log(err); }
      try { await saveProduct({ 'name': 'laptop', 'price': 1500, 'desc': 'gamer laptop' }); }
      catch (err) { console.log(err); }
    });
  }
});

Redémarrer le serveur

Invit de commande

C:\node-rest-graphql-hello>npm start

> node-rgh@1.0.0 start C:\node-rest-graphql-hello
> node server.js

SUCCEED : Connected to MongoDB [mongodb://localhost:27017/nrghDB]
SUCCEED : API REST and GrapQL server started on port [3000]
removing all products
saving new product {"name":"desktop","price":1000,"desc":"gamer desktop"}
saving new product {"name":"laptop","price":1500,"desc":"gamer laptop"}
Tester REST

Dans un client REST digne de ce nom

  • création d’un nouveau produit
Tester GraphQL

Dans le navigateur taper http://localhost:3000/api/graphql

  • création d’un nouveau produit

  • il a bien été créé

node  api  rest  graphql  mongo 
comments powered by Disqus