{ "super": "API" }

Un (tout) petit rappel

pour commencer

HTTP

GET /path/to/foo?type=bar HTTP/1.1
Host: api.example.com

url = scheme://host:port/path?query_string
(version simplifée)

méthode = GET, POST, PUT, DELETE, ...

Constat

  • les XHR sont souvent codées directement dans les vues,
  • les urls sont très souvent codées en dur,
  • parfois sous forme de variables,
  • comment faire lorsqu'on doit pointer différents environnements donc différentes url entre le test, la recette, la preprod, la prod ?
  • la création des query string se fait souvent "à la main",
  • etc,

 

=> qu'en est-il des bonnes pratiques ?

Petit aperçu

d'appels à une API ?

Quelques exemples avec jQuery

Construction du path








var request = $.ajax({
   url: "https://api.navitia.io/v1/" + path,
   method: "get",
   dataType: "json"
});
var point =  {
  lat: 48.874192
  lng: 2.353241
};

var path = "/coverage/fr-idf/coords/" + point.lng + ";" + point.lat + "/stop_schedules";

var request = $.ajax({
   url: "https://api.navitia.io/v1/" + path,
   method: "get",
   dataType: "json"
});

Appel API

var point =  {
  lat: 48.874192
  lng: 2.353241
};

var path = "/coverage/fr-idf/coords/" + point.lng + ";" + point.lat + "/stop_schedules";

var request = $.ajax({
   url: "https://api.navitia.io/v1/" + path,
   method: "get",
   dataType: "json",
   xhrFields: {
     withCredentials: true
   }
})

request.done(function (data, textStatus, jqXHR) { /* do something */ });
request.fail(function (jqXHR, textStatus, error) { /* do something */ });

var authKey = "XXXXXXX";
var lang = "fr";
var point =  {
  lat: 48.874192
  lng: 2.353241
};

Création d'une query string

Data

Building query: concatenate


var authKey = "XXXXXXX";
var lang = "fr";
var point =  {
  lat: 48.874192
  lng: 2.353241
};

var query = 
    "key=" + authKey + 
    "&lang=" + lang + 
    "&position=" + point.lat + "," + point.lng;

Building service url


var authKey = "XXXXXXX";
var lang = "fr";
var point =  {
  lat: 48.874192
  lng: 2.353241
};

var query = 
    "key=" + authKey + 
    "&lang=" + lang + 
    "&position=" + point.lat + "," + point.lng;

var path = "/position";

$.ajax({
   url: "http://api.what3words.com" + path + (query ? "?" + query : ""),
   method: "get",
   dataType: "json"
})

















var request = queryWords({
  lat: 48.874192
  lng: 2.353241
});

Création d'une query: refactoring

Il est toujours possible de faire un refactoring

On cache la complexité avec une fonction queryWords

function queryWords(point) {
    var authKey = "XXXXXXX";
    var lang = "fr";

    var path = "/position";
    var query = 
        "key=" + authKey + 
        "&lang=" + lang + 
        "&position=" + point.lat + "," + point.lng;
    
    return $.ajax({
       url: "http://api.what3words.com" + path + (query ? "?" + query : ""),
       method: "get",
       dataType: "json"
    });
}

var request = queryWords({
  lat: 48.874192
  lng: 2.353241
});







var point =  {
  lat: 48.874192
  lng: 2.353241
};

var path = "/coverage/fr-idf/coords/" + point.lng + ";" + point.lat + "/stop_schedules";

var request = $.ajax({
   url: "https://api.navitia.io/v1/" + path,
   method: "get",
   dataType: "json",



});

Authentification HTTP







var point =  {
  lat: 48.874192
  lng: 2.353241
};

var path = "/coverage/fr-idf/coords/" + point.lng + ";" + point.lat + "/stop_schedules";

var request = $.ajax({
   url: "https://api.navitia.io/v1/" + path,
   method: "get",
   dataType: "json"
});
var username = "username";
var password = "very-secret-password";

// generate base64 encoded string "Basic XXXXXX"
var authorization = btoa(username + ":" + password);

var point =  {
  lat: 48.874192
  lng: 2.353241
};

var path = "/coverage/fr-idf/coords/" + point.lng + ";" + point.lat + "/stop_schedules";

var request = $.ajax({
   url: "https://api.navitia.io/v1/" + path,
   method: "get",
   dataType: "json"
});
var username = "username";
var password = "very-secret-password";

// generate base64 encoded string "Basic XXXXXX"
var authorization = btoa(username + ":" + password);

var point =  {
  lat: 48.874192
  lng: 2.353241
};

var path = "/coverage/fr-idf/coords/" + point.lng + ";" + point.lat + "/stop_schedules";

var request = $.ajax({
   url: "https://api.navitia.io/v1/" + path,
   method: "get",
   dataType: "json",
   beforeSend: function (xhr) {
     xhr.setRequestHeader("Authorization", authorization);
   }
});

Problématiques

  • quelle gestion des URLS ?
  • comment construire le path ?
  • quelle gestion des queries ?
  • quelle gestion de l'authentification ?

Gestion des URL (1/2)

  • Protocole: http ou https
  • Gestion de différents environnements: recette, preprod, prod, ...
  • Maintien d'anciennes versions d'API​: v0, v1, ...

http://recette.domaine.tld
https://preprod.domaine.tld
https://prod.domaine.tld/v1
https://prod.domaine.tld/v2​

Gestion des URL (2/2)

  • Ou sous forme de variables ?

$.ajax({
    url: "http://recette.domaine.tld/foo"
});
var apiUrl = "http://recette.domaine.tld";

$.ajax({
  url: apiUrl + "/foo"
});
  • Hardcodées ?

Gestion des path

  • Comment gérer des paths paramétrés?

// PATH /coverage/fr-idf/coords/<resource>/stop_schedules
var path = "/coverage/fr-idf/coords/_resource_/stop_schedules";
  • Où sont stockés les paths ?

    • hardcodées ?

    • variables ?

// PATH /coverage/fr-idf/coords/<resource>/stop_schedules
var path = "/coverage/fr-idf/coords/_resource_/stop_schedules";








$.ajax({
    url: "https://api.navitia.io/v1" + path
});
// PATH /coverage/fr-idf/coords/<resource>/stop_schedules
var path = "/coverage/fr-idf/coords/_resource_/stop_schedules";

var point = {
  lat: 48.874192
  lng: 2.353241
};

path.replace("_resource_", point.lng + ";" + point.lat);

$.ajax({
    url: "https://api.navitia.io/v1" + path
});

Gestion des queries

Comment générer ce genre de query ?
 

 

// ?from=2.354255;48.869424&to=2.267189;48.8811491&datetime=20150624T190000
// &datetime_represents=arrival&max_duration_to_pt=300&max_nb_tranfers=2

Gestion des queries (1/3)

// ?from=2.354255;48.869424&to=2.267189;48.8811491&datetime=20150624T190000
// &datetime_represents=arrival&max_duration_to_pt=300&max_nb_tranfers=2

Par une concaténation de chaînes de caractères ?

 

var query = "?";
query += "from=" + from.lng + ";" + from.lat;
query += "&to=" + to.lng + ";" + to.lat;
query += "&datetime=" + moment.format(time);
query += "&datetime_represents=arrival";
query += "&max_duration_to_pt=" + duration;
query += "&max_nb_tranfers=" + transfers;

Gestion des queries (2/3)

var data = {
  from: "2.354255;48.869424",
  to: "2.267189;48.8811491",
  datetime: "20150624T190000",
  datetime_represents: "arrival",
  max_duration_to_pt: 300,
  max_nb_tranfers: 2
};

Hashmap pour les cas simples ?

var data = {
  from: "2.354255;48.869424",
  to: "2.267189;48.8811491",
  datetime: "20150624T190000",
  datetime_represents: "arrival",
  max_duration_to_pt: 300,
  max_nb_tranfers: 2
};

var queryArgs = [];

for (var param in data) {
    queryArgs.push(param + "=" + data[param]);
}

var query = "?" + queryArs.join("&");
var data = {
  from: "2.354255;48.869424",
  to: "2.267189;48.8811491",
  datetime: "20150624T190000",
  datetime_represents: "arrival",
  max_duration_to_pt: 300,
  max_nb_tranfers: 2
};

var queryArgs = [];

for (var param in data) {
    queryArgs.push(param + "=" + data[param]);
}

var query = "?" + queryArs.join("&");

// ?from=2.354255;48.869424&to=2.267189;48.8811491&datetime=20150624T190000
// &datetime_represents=arrival&max_duration_to_pt=300&max_nb_tranfers=2

Building query

Gestion des queries (3/3)

var from = {lng: 2.354255, lat: 48.869424};
var to = {lng: 2.267189, lat: 48.8811491}

var query = journeyQuery(from, to);

var journeyQuery = function (from, to) {
  var data = {
    datetime: "20150624T190000",
    datetime_represents: "arrival",
    max_duration_to_pt: 300,
    max_nb_tranfers: 2
  };

  var queryArgs = [];

  for (var param in data) {
    queryArgs.push(param + "=" + data[param]);
  }

  queryArgs.push("from=" + from.lng + ";" + from.lat);
  queryArgs.push("from=" + to.lng + ";" + to.lat);

  return "?" + queryArs.join("&");
}

Fonction pour les cas plus complexes ?

var from = {lng: 2.354255, lat: 48.869424};
var to = {lng: 2.267189, lat: 48.8811491}

var query = journeyQuery(from, to);

var from = {lng: 2.354255, lat: 48.869424};
var to = {lng: 2.267189, lat: 48.8811491}

var query = journeyQuery(from, to);

var journeyQuery = function (from, to) {
  var data = {
    datetime: "20150624T190000",
    datetime_represents: "arrival",
    max_duration_to_pt: 300,
    max_nb_tranfers: 2
  };

  var queryArgs = [];

  for (var param in data) {
    queryArgs.push(param + "=" + data[param]);
  }

  queryArgs.push("from=" + from.lng + ";" + from.lat);
  queryArgs.push("from=" + to.lng + ";" + to.lat);

  return "?" + queryArs.join("&");
}

// ?from=2.354255;48.869424&to=2.267189;48.8811491&datetime=20150624T190000
// &datetime_represents=arrival&max_duration_to_pt=300&max_nb_tranfers=2

Méthode HTTP

$.get()

$.post()

$.ajax({method: "put"})

$.ajax({method: "delete"})

$.ajax({method: "head"})

request.get()

request.post()

request.put()

request.del()

request.head()

Méthode HTTP

fetch(url, {method: "get"})

https://github.com/github/fetch

fetch(url, {method: "post"})

fetch(url, {method: "put"})

fetch(url, {method: "delete"})

fetch(url, {method: "head"})

Authentification

// https://github.com/what3words/w3w-javascript-wrapper/blob/master/what3words.js
var what3words = new function (language) {

	this.API_KEY = 'YOURAPIKEY'; 		// Change to your what3words API key
	this.language = language || 'en'; 	// Change to your default language

	// --

	this.postRequest = function (url, data, callback) {
		data.key = this.API_KEY;
		data.lang = this.language;
		$.post('http://api.what3words.com/' + url, data, callback, 'JSON');
	};
};

Authentification par un paramètre dans la query 

  • variable ?
  • codé en dur ?
https://api.what3words.com/position?key=YOURAPIKEY&lang=en&...

Authentification

ex. Basic Auth

  • variable ?
  • codé en dur ?
var username = "username";
var password = "very-secret-password";

// generate base64 encoded string "Basic XXXXXX"
var authorization = btoa(username + ":" + password);

$.ajax({
   // ...
});
var username = "username";
var password = "very-secret-password";

// generate base64 encoded string "Basic XXXXXX"
var authorization = btoa(username + ":" + password);

$.ajax({
   // ...
   
   beforeSend: function (xhr) {
     xhr.setRequestHeader("Authorization", authorization);
   }
});

Mais encore ...

  • Où stocker des headers dynamiques, par exemple 
    renvoyés par une API, type CSRF ?
  • Réponse serveur: callback ou Promise ?
  • Différents encodages pour différents appels:
    • form
    • json
    • xml
  • Gestion d'appels sur plusieurs API ?

Et pourquoi pas 

 

  • des fichiers de configuration
  • des appels de méthode simples
  • aucune manipulation des URLS 
  • abstraction sur les appels d'XHR

superapi

Sans superapi

// PATH /coverage/fr-idf/coords/<resource>/stop_schedules
var path = "/coverage/fr-idf/coords/_resource_/stop_schedules";

var point = {
  lat: 48.874192
  lng: 2.353241
};

path.replace("_resource_", point.lng + ";" + point.lat);

var request = $.ajax({
    url: "https://api.navitia.io/v1" + path
});

request.done(function() {
    // do something
});

superapi

En 3 points ...

superapi

Au commencement, une configuration

1. configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      path: "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      method: "GET" 
    }
  },
  options: {
    type: "form"
  },
  headers: {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }
}

fichier JSON

superapi

Ensuite, la mise en place

2. initialisation


var api = new superapi.default.Api();
  




var api = new superapi.default.Api();
  
api.configure("navitia", navitiaConf);



superapi

Fire!

3. appel XHR

api.navitia.stopSchedule({
  params: point,
  query: {
    distance: distance
  }
});
api.navitia.stopSchedule();

superapi

Quelques explications

1. superapi: configuration

{
  "baseUrl": "",
  "services": {},
  "headers": {},
  "options": {}
}

1. superapi: configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      "path": "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      "method": "GET",
      "options": {
        "type": "json"
      }
    }
  },
  "headers": {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }
}

API endpoint

1. superapi: configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      "path": "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      "method": "GET",
      "options": {
        "type": "json"
      }
    }
  },
  "headers": {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }
}

Services

services est un hash map de services configurés

1. superapi: configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      "path": "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      "method": "GET",
      "options": {
        "type": "json"
      }
    }
  },
  "headers": {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }
}

Service id

  • Le service id doit être préférablement en camelCase
  • Le service id est le nom de la fonction générée (curry)

1. superapi: configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      "path": "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      "method": "GET",
      "options": {
        "type": "json"
      }
    }
  },
  "headers": {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }
}

Service path

  • Peut-être un chemin paramétré
  • Les paramètres doivent suivre le patron :param
  • Ce patron est configurable

1. superapi: configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      "path": "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      "method": "GET",
      "options": {
        "type": "json"
      }
    }
  },
  "headers": {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }
}

Service method

  • Valeur par défaut: "GET"
  • Support des différents verbes HTTP

1. superapi: configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      "path": "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      "method": "GET",
      "options": {
        "type": "json"
      }
    }
  },
  "headers": {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }
}

Service options

  • hash map des différentes options par défaut

1. superapi: configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      "path": "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      "method": "GET",
      "options": {
        "type": "json"
      }
    }
  },
  "headers": {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }
}

global headers

1. superapi: configuration

{
  "baseUrl": "https://api.navitia.io/v1",
  "services": {
    "stopSchedules": {
      "path": "/coverage/fr-idf/coords/:lng;:lat/stop_schedules",
      "method": "GET"
    }
  },
  "options": {
    "type": "json",
    "accept": "json"
  },
  "headers": {
    "Authorization": "Basic ZWRkNWYxODQt...."
  }   
}

global options

Also supported : global options

2. superapi: initialisation











  var api = new superapi.default.Api();

  api.withSuperagent(superagent);
  
  api.configure("navitia", navitiaConf);



1. setup API wrapper

3. add configuration

// controllers/api.js
define([
  "superapi",
  "superagent",
  "json!config/api/navitia.json"
],
function (superapi, superagent, navitiaConf) {
  
  "use strict";

  var api = new superapi.default.Api();

  api.withSuperagent(superagent);
  
  api.configure("navitia", navitiaConf);

  return api;
});

2. add XHR agent

3. superapi: appel XHR








  return function(point, distance) {
    return api.navitia.stopSchedule({
      params: point,
      query: {
        distance: distance
      }
    });
  };

XHR call

Exemple avec utilisation d'un wrapper

// api/stopSchedule.js
define([
  "controllers/api"
],
function (api) {
  "use strict";

  return function(point, distance) {
    return api.navitia.stopSchedule({
      params: point,
      query: {
        distance: distance
      }
    });
  };
});

3. superapi: appel XHR








  var point = {
    lat: 48.874192,
    lng: 2.353241
  };
    
  stopSchedule(point, 500).then(/* */);

calling our XHR wrapper

// somewhere in your code
define([
  "api/stopSchedule"
], function (stopSchedule) {
  
  // ...

  var point = {
    lat: 48.874192,
    lng: 2.353241
  };
    
  stopSchedule(point, 500).then(/* */);

  // ...

});

superapi

En quelques lignes

superapi

  • La promesse d'une réponse
  • Gestion de multiples API
  • Modification de la requête

La promesse d'une réponse

// somewhere in your code
define([
  "api/stopSchedule"
], function (stopSchedule) {
  
  // ...

  var point = {
    lat: 48.874192,
    lng: 2.353241
  };
    
  stopSchedule(point, 500)
    .then(function (res) {
      // success
    })
    .catch(function (error) {
      // error!
    });

  // ...

});

returning a Promise

sous forme d'une promesse

// api/stopSchedule.js
define([
  "controllers/api"
],
function (api) {
  "use strict";

  return function(point, distance) {
    return api.navitia.stopSchedule({
      params: point,
      query: {
        distance: distance
      }
    });
  };
});

La promesse d'une réponse

// somewhere in your code
define([
  "api/stopSchedule"
], function (stopSchedule) {
  
  // ...

  var point = {
    lat: 48.874192,
    lng: 2.353241
  };
    
  var callback = function (err, res) {
    // do something
  };

  stopSchedule(point, 500, callback);

  // ...

});

passing a callback

ou d'un bon vieux callback

// api/stopSchedule.js
define([
  "controllers/api"
],
function (api) {
  "use strict";

  return function(point, distance, cb) {
    return api.navitia.stopSchedule({
      params: point,
      query: {
        distance: distance
      },
      callback: cb
    });
  };
});

Gestion de multiples API

il suffit d'ajouter autant de configurations que nécessaires












  var api = new superapi.default.Api();

  api.withSuperagent(superagent);
  
// controllers/api.js
define([
  "superapi",
  "superagent",
  "json!config/api/navitia.json",
  "json!config/api/uber.json"
],
function (superapi, superagent, navitia, uber) {
  
  "use strict";

  var api = new superapi.default.Api();

  api.withSuperagent(superagent);
  
  api.configure("navitia", navitia);
  api.configure("uber", uber);

  return api;
});











  var api = new superapi.default.Api();

  api.withSuperagent(superagent);
  
  api.configure("navitia", navitia);











  var api = new superapi.default.Api();

  api.withSuperagent(superagent);
  
  api.configure("navitia", navitia);
  api.configure("uber", uber);

Modification de la requête

Accéder à la requête

api.navitia.stopSchedule({
  params: point,
  query: {
    distance: distance
  }
})
api.navitia.stopSchedule({
  params: point,
  query: {
    distance: distance
  },
  edit: function (req) {
    // before xhr call
  }
})

ROADMAP

Actuellement

version 0.10.5

  • Support callback / promise
  • Une seule configuration
  • Gestion des headers dynamiques
  • Un seul agent supporté : superagent (historique)

Juillet

version 1.0.0

  • Réécriture complète
  • La même chose que la 0.10.5 :)
  • Multiples configurations
  • Multiples agents ($.ajax, ...)
  • Meilleure documentation
  • plugins API ?

Et Backbone ?

backbone-superapi-sync

https://github.com/stephanebachelier/backbone-superapi-sync

backbone-superapi-sync

define([
  'backbone',
  'backbone.superapiSync',
  'controllers/api',
],
function (Backbone, backboneSuperapiSync, superapi) {
  'use strict';

  return Backbone.Collection.extend({
    sync: function (method, model, options) {
      return backboneSuperapiSync(superapi).call(this, method, model, options);
    }
  });
});

backbone-superapi-sync

define([
  'backbone',
  'controllers/api'
],

function (Backbone, api) {
  'use strict';

  return Backbone.Collection.extend({
    url: function () {
      var query = {
        q: this.q,
        offset: this.offset,
        limit: this.size,
        'filters[type]': 'entities',
      };

      return api.buildUrl('search', undefined, query);
    }
  });
});

/!\ code 0.10.5

Encore plus fort

APItools.com

PROXY sur VOS API

  • Réécriture de HTTP queries
  • Injection de headers HTTP
  • Cache
  • Un bon moyen de pallier les limites d'appels
  • Réécriture de réponse

PROXY sur VOS API

  • Open source
  • Stack en ruby
  • Des middlewares en LUA

DASHBOARD

PIPELINE

Your app

AN API

Some middlewares

{ "super" : "API" }

By stephane bachelier

{ "super" : "API" }

  • 2,089