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
- GraphQL (voir aussi graphql-js)
- express-graphql.
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éesmongo
: 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
- Redémarrer le serveur
- Taper l’URL http://localhost:3000 dans le navigateur
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}
, commegraphiql=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
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
Tester GraphQL
Dans le navigateur taper http://localhost:3000/api/graphql