adesso Blog

Lorsque nous nous interrogeons sur le langage ou le framework à utiliser pour implémenter une API REST, des technologies comme Java et Spring nous semblent certainement très en avance. Et ce n’est pas sans raison. Dans l’univers Java, de nombreux frameworks et bibliothèques ont fait leurs preuves et offrent un large éventail de fonctions. Dans cet article, nous voulons donner un aperçu d’un langage de programmation encore jeune, Rust, et implémenter une petite API REST avec le framework web Actix.

Les prérequis

Comme je veux surtout présenter le framework (et non le langage en lui-même), il est important que vous ayez des connaissances de base du langage. Le livre The Rust Programming Language est un bon support de départ.

Mise en place d’un nouveau projet

Première étape : la création d’un nouveau projet. Pour cela, nous utilisons le gestionnaire de paquets Cargo. Pour ce faire, nous exécutons la commande cargo new <projekt-name> --bin dans notre espace de travail. Pour utiliser Actix, nous spécifions la dépendance dans le fichier Cargo.toml (similaire à un fichier build.gradle ou pom.xml).

	
	[dependencies]
	actix-web = "1.0.8" // latest version
	

Notre API REST

Dans notre exemple d’application, nous voulons gérer des personnes. Pour cela, nous allons créer une petite API REST simple. Nous voulons pouvoir créer, supprimer et modifier une personne. Nous voulons également définir des points d’extrémité qui nous fournissent toutes ou certaines personnes (identifiables par leur ID). Nous devrons donc implémenter cinq points d’extrémité différents, voir ci-dessous :

• GET /persons
• GET /persons/id
• POST /persons
• PUT /persons/id
• DELETE /persons/id

Gestionnaire de requêtes

Pour pouvoir accepter les requêtes à notre API, nous devons implémenter des gestionnaires de requêtes (également abrégés en GR dans la suite du présent document). Un GR est une fonction qui accepte zéro ou plusieurs paramètres et retourne une valeur qui implémente le trait Responder. Les types et les structures de données qui implémentent le trait Responder sont ensuite convertis par le framework en réponse HTTP. Cela garantit que seuls les types qui peuvent être convertis en réponse HTTP sont renvoyés.

Exemple de gestionnaire de requêtes

Prenons un exemple de gestionnaire de requêtes. Pour ce faire, définissons une fonction ne prenant aucun paramètre pour l’instant et renvoyant une valeur qui implémente le trait Responder. Actix-Web propose quelques implémentations par défaut du trait Responder pour les types et les structures de données courants. Il en va de même pour le type de données Chaîne. Notre GR pourrait alors ressembler à ceci :

	
	fn request_handler() -> impl Responder {
	"Hello world!".to_owned()
	}
	

Nous devons à présent définir sous quelle requête cette fonction doit être appelée. Mais commençons par examiner le serveur HTTP et l’application d’Actix-Web. to_owned() est une caractéristique de Rust et nous fournit ici la chaîne souhaitée au lieu de sa référence.

HttpServer et instance d’application

À l’aide du framework, nous sommes en mesure de créer un serveur HTTP dans lequel s’exécute notre application. Pour cela, nous devons créer une nouvelle instance de la structure HttpServer et spécifier où elle doit s’exécuter (adresse IP et port).

Nous le spécifions au moyen de la méthode bind(). Nous créons ensuite notre instance d’application dans le serveur HTTP. Ici, nous précisons également la requête pour laquelle notre exemple de GR doit être appelé.

Pour notre exemple, nous pourrions simplement spécifier le chemin racine de l’URL et l’appeler avec un GET. Nous obtenons ainsi la fonction principale suivante :

	
	fn main() {
	    HttpServer::new(|| {
	  App::new()
	  .route("/", web::get().to(request_handler))
	})
	.bind("127.0.0.1:8099")
	.unwrap()
	.run()
	.unwrap();
	}
	

Notre serveur s’exécute à présent en local sur le port 8099. Le GR est appelé à chaque fois qu’une requête GET survient sur la route /. Nous avons déjà créé une application web Actix complète et nous pouvons l’exécuter. Pour cela, nous appelons le localhost sur le port 8099 du navigateur et obtenons la sortie suivante :

Example Request Handler
Sucre syntaxique

Au lieu de définir la route et le verbe HTTP à l’aide de méthodes supplémentaires, nous avons également la possibilité de les étiqueter à l’aide d’attributs macro. Nous pouvons nous les représenter comme des annotations Java. Pour notre gestionnaire de requêtes, cela ressemblerait à ceci :

	
	#[get("/")]
	fn request_handler() -> impl Responder {
	"Hello world!".to_owned()
	}
	

De cette manière, nous pouvons nous passer de la fonction principale .route("/", web::get().to(request_handler)) et appeler à la place la méthode service() de l’instance de l’application afin de transmettre notre GR en tant que paramètre :

	
	App::new().service(request_handler)
	

Sur le plan sémantique, les deux procédures sont totalement identiques. Les attributs macro sont donc des sucres syntaxiques qui facilitent la programmation.

Nous savons maintenant comment définir et intégrer un gestionnaire de requêtes dans l’application. Pour utiliser l’API REST de manière judicieuse, nous avons besoin d’un modèle de données.

Notre modèle de données

Nous veillerons à ce que notre modèle de données reste aussi simple que possible pour éviter toute complexité inutile. Notre structure de personnes est donc la suivante :

	
	pub struct Person {
	id: u32,
	name: String,
	age: u32
	}
	

Une personne a un ID, un nom et un âge. Nous fournissons un constructeur, un accesseur (getter) et un mutateur (setter):

	
	impl Person {
	pub fn new(id: u32, name: String, age: u32) -> Self {
	Person { id, name, age }
	}
	    pub fn id(&self) -> u32 {
	self.id
	}
	pub fn name(&self) -> &String {
	        &self.name
	}
	pub fn age(&self) -> u32 {
	self.age
	}
	    pub fn set_name(&mut self, name: String) {
	        self.name = name;
	}
	pub fn set_age(&mut self, age: u32) {
	self.age = age;
	    }
	}
	

Implémentation de l’API REST

Dans un premier temps, nous implémentons un point d’extrémité REST qui nous fournit toutes les personnes. Celles-ci doivent nous être livrées par le serveur sous forme d’objets JSON. Pour la communication via JSON, Actix-Web nous propose une structure correspondante que nous devons importer.

	
	use actix_web::web::{Json};
	

Nous pouvons alors définir notre gestionnaire de requêtes comme suit :

	
	#[get("/persons")]
	pub fn get_all() -> Json<Vec<Person>> {
	// ...
	}
	

Cette fonction est appelée à chaque fois que nous recevons une requête GET sur le chemin /persons. Elle nous donne la représentation JSON d’un vecteur d’instances de personnes. Par souci de simplicité, nous ne voulons pas communiquer avec une base de données ou le système de fichiers à ce stade. Nous supposons que la couche de persistance nous fournit un vecteur de personnes.

	
	#[get("/persons")]
	pub fn get_all() -> Json<Vec<Person>> {
	 // Communication avec la base de données ou le système de fichiers
	// Nous initialisons le vecteur à ce stade manuellement
	let micheal = Person::new(1, "Micheal".to_owned(), 32);
	let frank = Person::new(2, "Frank".to_owned(), 28);
	let persons = vec![micheal, frank];
	    Json(persons)
	}
	

Malheureusement, notre code ne compile pas pour l’instant. Actix-Web ne sait pas comment créer un objet JSON à partir de notre structure de personnes. À ce stade, nous devons ajouter à notre projet une autre bibliothèque appelée « serde » et permettant de sérialiser et de désérialiser des objets JSON. Nous complétons donc notre Cargo.toml :

	
		[dependencies]
		actix-web = "1.0.8"
		serde = "1.0.101"
	

Serde permet de sérialiser très facilement des structures autodéfinies en objets JSON ou désérialiser à nouveau les objets JSON correspondants. Pour cela, il suffit d’étendre notre structure de personnes comme suit :

	
	#[derive(Serialize, Deserialize)]
	struct Person {
	// ...
	}
	

Serde convertit à présent les valeurs de type u32 et String en objets JSON. Comme notre structure de personnes ne comprend que ces types, nous pouvons spécifier les traits Serializeet et Deserializeen utilisant la macro derive. Notre code est déjà en cours de compilation.

Si nous nous adressons à présent au point d’extrémité, le serveur fournit la réponse suivante :

Get persons request
Paramètres d’URL dynamiques

Dans l’étape suivante, nous allons implémenter un point d’extrémité pour lire une personne au moyen de son ID. Pour ce faire, nous devons transmettre des paramètres d’URL dynamiques à notre gestionnaire de requêtes. Dans Actix-Web, les paramètres dynamiques sont indiqués entre parenthèses. La route de notre requête GET avec un ID de personne ressemble donc à ceci :

	
	#[get("/persons/{id}")]
	

Nous devons ensuite transmettre l’ID spécifié dans l’URL au GR en tant que paramètre d’entrée. Nous voulons également que la personne soit livrée en tant qu’objet JSON :

	
	#[get("/persons/{id}")]
	pub fn get(id: u32) -> Json<Person> {
	}
	

Or, nous obtenons une erreur de compilation lorsque nous essayons de construire le projet. Ceci est dû au fait que chaque paramètre d’entrée d’un GR doit implémenter le trait FromRequest. Ce n’est pas le cas pour le type u32.

Heureusement, Actix-Web nous aide aussi sur ce point. Il fournit une structure Path servant de wrapper aux paramètres de la requête. Nous devons donc wrapper notre ID dans un chemin :

	
	#[get("/person/{id}")]
	pub fn get(id: Path<u32>) -> Json<Person> {
	}
	

Le type Path implémente le trait Deref. Ainsi, pour accéder à notre valeur de type u32, la variable doit être déréférencée. Ici aussi, nous ne voulons d’abord retourner qu’une valeur fictive.

On peut cependant imaginer qu’à ce stade, une communication est établie avec une base de données et qu’une personne avec l’ID approprié est fournie.

	
	#[get("/person/{id}")]
	pub fn get(id: Path<u32>) -> Json<Person> {
	let person = Person::new(*id, "Tom".to_owned(), 38);
	    Json(person)
	}
	
Créer une personne

Pour créer une personne, nous devons préciser son nom et son âge dans une requête correspondante. Cependant, nous ne voulons pas spécifier d’ID explicitement : c’est normalement la tâche de la base de données. Nous devons donc définir une nouvelle structure, que nous appelons ici NewPerson :

	
	#[derive(Serialize, Deserialize)]
	pub struct NewPerson {
	pub name: String,
	pub age: u32
	}
	

Là encore, nous devons utiliser Serialize et Deserialize en utilisant la macro derive pour spécifier la structure correspondante en tant que corps JSON dans une requête. Notre GR pour la création d’une personne ressemble à ceci :

	
	#[post("/persons")]
	pub fn create(person: Json<NewPerson>) -> String {
	// Création de la personne dans la base de données ou dans un fichier ou similaire
	}
	

Pour simplifier, nous ne retournerons dans un premier temps qu’une seule chaîne. Celle-ci pourrait indiquer si la création de la personne a réussi ou non.

En cas d’erreur, un message d’erreur correspondant peut également être émis.

Modifier une personne

Pour modifier une personne, il faut modifier soit son nom, soit son âge, soit les deux. La requête doit également indiquer les changements apportés au corps de JSON. À ce stade, nous devons donc définir une nouvelle structure répondant à nos exigences :

	
	#[derive(Serialize, Deserialize)]
	pub struct UpdatePerson {
	pub name: Option<String>,
	pub age: Option<u32>
	}
	

La seule différence avec la structure NewPerson est que nous ne sommes pas obligés à préciser le nom ou l’âge. Notre gestionnaire de requêtes pourrait ressembler à ceci :

	
	#[put("/persons/{id}")]
	pub fn update(id: Path<u32>, person: Json<UpdatePerson>) -> String {
	// ...
	}
	
Supprimer une personne

La suppression d’une personne ne nécessite pas de nouvelles fonctions ou techniques du framework. Sans autre précision, le gestionnaire de requêtes ressemble à ceci :

	
	#[delete("/persons/{id}")]
	pub fn delete(id: Path<u32>) -> String {
	// ...
	}
	

Nous avons à présent défini tous les gestionnaires de requêtes pour notre API REST.

Conclusion

Dans cet article, nous avons appris comment implémenter une API REST avec le framework web Actix. Nous avons déjà utilisé des fonctions importantes du framework :

• Gestionnaire de requêtes
• Routes
• Paramètres d’URL dynamiques
• Communication via JSON

Avec quelques connaissances de Rust, s’initier au framework n’est pas très difficile. La documentation officielle constitue la meilleure source de référence pour obtenir des informations plus approfondies.

Bien sûr, le framework offre bien d’autres fonctionnalités que celles décrites dans cet article. Nous consacrerons d’autres articles de blog à des aspects comme la communication avec une base de données, la gestion des erreurs et les tests unitaires ou d’intégration.

En guise de teaser, les lecteurs intéressés trouveront une version étendue du code dans ce repository Github, qui implémente une communication rudimentaire avec le système de fichiers.

Pour consulter un exemple d’application complète avec Actix-Web, cliquez par exemple ici. Un ORM y est également utilisé pour sauvegarder les personnes dans une base de données.

Photo  Kilian Krause

Auteur Kilian Krause

Kilian Krause est un étudiant salarié membre de l’équipe Open Source d’adesso à Dortmund.

Sauvegarder cette page. Supprimer cette page.