Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici

Création d’une API

Définition

Une API (Application Programming Interface) est une interface de programmation applicative. Dans la pratique, cela correspond à un programme informatique qui reçoit des requêtes et renvoie des informations, effectue des traitements et renvoie une réponse selon la requête envoyée. Elle est souvent couplée à une source de données comme une base de données.

Son but est d’agir comme une boîte noire aux yeux de l’utilisateur : en effet, les requêtes effectuées vers une API correspondent à des actions (des demandes), l’API va effectuer ces actions et recherches via des fonctionnalités développées dans son code puis renvoyer uniquement le résultat final à l’utilisateur.

En termes de projet, une API est composée de différentes fonctionnalités regroupées dans des services. En Scala, il existe plusieurs librairies pour définir une API mais on va se focaliser dans ce chapitre sur une librairie simple et efficace : AkkaHttp.

Akka HTTP

Akka HTTP est un framework permettant d’exposer une API en Scala ainsi que d’appeler d’autres API.

Le principe est de créer des routes, où chaque route correspond à un chemin d’accès et comprend une ou plusieurs directives, chacune représentant une opération à effectuer. Un serveur, créé avec ces routes, ouvre un port prêt à recevoir des requêtes.

Pour intégrer Akka HTTP dans un projet Scala, il faut ajouter les trois dépendances suivantes dans le fichier build.sbt :

val AkkaVersion = "2.5.13" 
val AkkaHttpVersion = "10.1.3" 
 
libraryDependencies ++= Seq( 
 "com.typesafe.akka" %% "akka-actor" % AkkaVersion, 
 "com.typesafe.akka" %% "akka-stream" % AkkaVersion, 
 "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion 
) 

Dans notre exemple, on utilise la version 10.2.6 de AkkaHttp qui est compatible avec la version 2.6.15 de Akka.

La librairie akka-http est entièrement indépendante des modules Akka mais la création d’un serveur http nécessite les librairies akka-actor et akka-stream.

Pour transformer le corps des requêtes ainsi que leurs réponses, on favorise le formatage en JSON. Pour pouvoir transformer les classes Scala en JSON et vice-versa, on ajoute comme dépendance...

Configuration

Dans ce chapitre, on utilise une configuration basique pour le serveur et le client http. Cette configuration est utilisée par défaut, mais pour plus de lisibilité, on l’ajoute dans le fichier build.sbt.

La configuration se trouve dans un objet akka.http présent à la fois dans l’objet server et l’objet client. Il existe également une configuration nommée parsing qui contient les paramètres pour le serveur et le client.

1. Serveur

Le paramètre idle-timeout correspond au temps à partir duquel une connexion inactive est fermée. Par défaut, sa valeur est de 60 secondes et si on veut supprimer ce comportement, il faut spécifier sa valeur à "infinite".

idle-timeout = 60 s 

Le paramètre request-timeout correspond au temps par défaut à partir duquel une réponse est considérée comme en timeout (délai dépassé). Le timer se lance à partir de la fin de la réception de la requête. Par défaut, sa valeur est de 20 secondes. 

request-timeout = 20 s 

Le paramètre default-http-port correspond au port à utiliser lorsqu’aucun autre port n’est spécifié. Par défaut, c’est le port 80 qui est utilisé.

default-http-port = 80 

Le paramètre pipelining-limit correspond au nombre maximum de requêtes acceptées...

cURL

Dans tout ce chapitre, nous allons utiliser la commande curl afin de requêter notre serveur et afficher la réponse complète.

Pour installer cURL, il suffit de le télécharger pour le système d’exploitation correspondant sur le site https://curl.se/download.html.

1. get

Par défaut, le type de requête effectuée est le get. En lançant la commande curl suivie de l’URL souhaitée, on effectue une requête get vers cette URL.

~ curl https://jsonplaceholder.typicode.com/todos/1 
{ 
 "userId": 1, 
 "id": 1, 
 "title": "delectus aut autem", 
 "completed": false 
}% 

Pour obtenir la réponse complète, notamment le code de retour, les headers et le corps, il faut renseigner l’option -i :

~ curl https://jsonplaceholder.typicode.com/todos/1 
HTTP/2 200 
[...] 
{ 
 "userId": 1, 
 "id": 1, 
 "title": "delectus aut autem", 
 "completed": false 
}% 

Dans cet extrait de code, une bonne partie de la requête représentée par [...] a été omise pour se concentrer sur le plus important.

2. post

Pour effectuer un post, on renseigne l’option -d ou --data suivie du corps de la requête correspondant souvent à l’objet...

Partie serveur

Dans cette section, nous allons découvrir comment créer un serveur qui reçoit des requêtes. Le code est disponible dans la classe ServeurApp et dans le paquet eni.chapitres.chapitre5.

1. Définition

Pour créer un serveur, il faut définir plusieurs éléments :

  • Un acteur système implicite.

  • Un contexte d’exécution implicite.

  • Des routes.

Un acteur système permet de créer plusieurs threads et de répartir les tâches sur ces derniers. Dans notre exemple, nous n’allons pas gérer d’acteurs différents et utiliserons l’acteur système par défaut nommé avec la classe.

implicit val system: ActorSystem = ActorSystem("serveur-app") 

Le contexte d’exécution est nécessaire dans le cas d’opérations asynchrones et d’utilisation de Future. On utilise le contexte fourni par l’acteur système qui permet de répartir les tâches sur les threads disponibles.

implicit val executor: ExecutionContext = system.dispatcher 

Une route est représentée par un chemin et une action correspondante. Le type Route est défini comme une fonction qui prend en entrée un contexte et qui renvoie un résultat de type Future :

type Route = RequestContext => Future[RouteResult] 

Lorsqu’une route reçoit une requête, matérialisée par le type RequestContext, il est possible de renvoyer une réponse en effectuant une des directives suivantes :

  • Compléter la requête avec un code de statut et un corps optionnel grâce à la méthode complete.

  • Rejeter la requête avec une erreur de validation grâce à la méthode reject.

  • Compléter la requête en échec avec une Exception grâce à la méthode fail.

De par sa définition, une route peut être tout simplement décrite par une fonction :

val route = { contexte => 
 contexte.complete("Complétion de la route") 
} 

Cette route sera liée au chemin "/" et à la méthode http get. Elle renvoie une réponse contenant le message spécifié et un code de retour 200 (Ok).

Pour créer le serveur à partir de ces éléments...

Partie client

Dans cette section, nous allons découvrir comment créer un client qui envoie des requêtes. Le code est disponible dans la classe ClientApp et le serveur est défini dans la classe IClientService.

1. Définition du client

Pour créer un client, il faut définir plusieurs éléments :

  • Un acteur système implicite.

  • Un contexte d’exécution implicite.

Comme dans la partie serveur, nous allons prendre un ActorSystem et un ExecutionContext par défaut.

implicit val system: ActorSystem = ActorSystem("client-app") 
implicit val executor: ExecutionContext = system.dispatcher 

Pour pouvoir lancer des requêtes, on crée un objet de type HttpExt avec le constructeur Http.

private val http = Http() 

Avec cet objet, on peut exécuter des requêtes de type HttpRequest avec la méthode singleRequest.

2. Création d’une requête

Une requête est représentée par un objet de type HttpRequest. Elle est constituée des éléments suivants :

  • Une méthode, par défaut Get.

  • Une URI, par défaut définie à "/".

  • Une séquence de headers, par défaut Nil.

  • Une entité, par défaut un corps vide.

  • Un protocole HTTP, par défaut "HTTP/1.1".

Les méthodes Http sont listées dans l’objet HttpMethods ; on y retrouve les méthodes abordées dans la partie cURL :

  • GET

  • POST

  • PUT

  • DELETE

  • etc.

Les headers sont représentés par le trait RequestHeader ; il existe des classes qui décrivent des headers particuliers comme :

  • Authorization

  • Accept

  • Cookie

Pour tous les headers qui ne sont pas définis dans une classe, on utilise la classe RawHeader qui prend comme arguments un nom et une valeur.

Une entité est représentée par la classe HttpEntity qui prend comme arguments un type de contenu et le contenu lui-même sous forme de String ou de byte[]. Les types de contenus sont définis dans l’objet ContentTypes qui contient les types suivants :

  • application/json

  • text/plain(UTF-8)

  • etc.

Le protocole Http est représenté par la classe HttpProtocol et tous les protocoles disponibles sont définis dans l’objet HttpProtocols :

  • HTTP/1.0

  • HTTP/1.1

  • HTTP/2.0

3. Traitement de la réponse...