Voici un tutoriel approfondi sur la création d'une API REST avec Rust :
Pourquoi API REST avec Rust ?
Un développement web moderne nécessite une communication efficace entre différents composants logiciels. Les API REST fournissent une interface standard pour cette communication, en permettant aux systèmes de s'interfaçer via des requêtes HTTP simples et claires.
Dans un contexte concret, imaginez que vous développiez une application web avec une architecture microservices. Chaque service doit pouvoir communiquer efficacement avec les autres. Les API REST sont parfaitement adaptées à ce besoin, offrant une interface uniforme pour accéder aux ressources et effectuer des opérations CRUD (Create, Read, Update, Delete).
Prerequis
Connaissances requises :
- Rust : base de syntaxe, structures de données, traits.
- HTTP : concepts de requêtes et réponses.
- Gestion de projet : Cargo.
Outils à installer :
- Rust : https://www.rust-lang.org/tools/install
- Cargo (généralement installé avec Rust) : vérifiez en tapant
cargo --versiondans votre terminal. - Serveur HTTP local : pour tester rapidement l'API, vous pouvez utiliser
hyperlocalouwarp.
Concepts fondamentaux
1. Serde
Serde est une bibliothèque populaire de Rust utilisée pour la sérialisation et la désérialisation des données. Elle permet d'convertir facilement entre des structures Rust et des formats comme JSON, YAML, etc.
// Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
rust
// src/main.rs
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct User {
id: u64,
name: String,
}
fn main() {
let user = User { id: 1, name: "Alice".to_string() };
let json = serde_json::to_string(&user).unwrap();
println!("User as JSON: {}", json);
}
2. Actix-web
Actix-web est un framework web performant et simple à utiliser pour construire des API REST en Rust.
// Cargo.toml
[dependencies]
actix-web = "4.0"
serde = { version = "1.0", features = ["derive"] }
rust
// src/main.rs
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
async fn index() -> impl Responder {
let user = User { id: 1, name: "Alice".to_string() };
HttpResponse::Ok().json(user)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
3. Middleware
Les middleware sont des fonctions qui s'exécutent entre la réception de la requête et sa réponse. Ils peuvent être utilisés pour effectuer diverses tâches, comme l'authentification, le logging, etc.
// Cargo.toml
[dependencies]
actix-web = "4.0"
rust
// src/main.rs
use actix_web::{middleware, web, App, HttpResponse, HttpServer, Responder};
async fn auth_middleware<B>(req: actix_web::HttpRequest<B>, next: actix_web::dev::ServiceHandler<actix_web::http::Request<B>>) -> std::future::LocalBoxFuture<'static, Result<HttpResponse, actix_web::Error>> {
println!("Authenticating request...");
next.handle(req).boxed_local()
}
async fn index() -> impl Responder {
HttpResponse::Ok().body("Hello, world!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.wrap(middleware::from_fn(auth_middleware))
.route("/", web::get().to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Mise en pratique : projet fil rouge
Projet : Gestionnaire de tâches
Le but est de créer un simple gestionnaire de tâches avec une API REST. L'application permettra d'afficher, ajouter, mettre à jour et supprimer des tâches.
Étape 1: Structure du projet
mkdir task-manager
cd task-manager
cargo new --bin task-manager
Étape 2 : Ajout des dépendances
Modifier Cargo.toml :
[dependencies]
actix-web = "4.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Étape 3 : Création du modèle de données
Créer un fichier src/models.rs :
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Task {
id: u64,
title: String,
completed: bool,
}
Étape 4 : Création du service de tâches
Créer un fichier src/services.rs :
use std::collections::HashMap;
use super::models::{Task, TaskId};
pub struct TaskService {
tasks: HashMap<TaskId, Task>,
}
impl TaskService {
pub fn new() -> Self {
TaskService {
tasks: HashMap::new(),
}
}
pub fn create_task(&mut self, title: String) -> TaskId {
let id = self.tasks.len() as u64 + 1;
let task = Task {
id,
title,
completed: false,
};
self.tasks.insert(id, task);
id
}
pub fn get_task(&self, id: &TaskId) -> Option<&Task> {
self.tasks.get(id)
}
pub fn update_task(&mut self, id: &TaskId, title: Option<String>, completed: Option<bool>) -> Result<(), String> {
if let Some(task) = self.tasks.get_mut(id) {
if let Some(title) = title {
task.title = title;
}
if let Some(completed) = completed {
task.completed = completed;
}
Ok(())
} else {
Err("Task not found".to_string())
}
}
pub fn delete_task(&mut self, id: &TaskId) -> Result<(), String> {
if self.tasks.remove(id).is_some() {
Ok(())
} else {
Err("Task not found".to_string())
}
}
}
Étape 5 : Création des routes API
Créer un fichier src/routes.rs :
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use super::services::TaskService;
async fn index(data: web::Data<TaskService>) -> impl Responder {
let tasks = data.tasks.values().cloned().collect::<Vec<_>>();
HttpResponse::Ok().json(tasks)
}
async fn create_task(data: web::Data<TaskService>, task_data: web::Json<NewTask>) -> impl Responder {
let id = data.create_task(task_data.title.clone());
let task = data.get_task(&id).unwrap();
HttpResponse::Created().json(task)
}
async fn get_task(data: web::Data<TaskService>, task_id: web::Path<u64>) -> impl Responder {
if let Some(task) = data.get_task(&task_id.into_inner()) {
HttpResponse::Ok().json(task)
} else {
HttpResponse::NotFound().body("Task not found")
}
}
async fn update_task(data: web::Data<TaskService>, task_id: web::Path<u64>, task_data: web::Json<UpdateTask>) -> impl Responder {
if let Ok(_) = data.update_task(&task_id.into_inner(), task_data.title.clone(), task_data.completed) {
HttpResponse::Ok().body("Task updated")
} else {
HttpResponse::NotFound().body("Task not found")
}
}
async fn delete_task(data: web::Data<TaskService>, task_id: web::Path<u64>) -> impl Responder {
if let Ok(_) = data.delete_task(&task_id.into_inner()) {
HttpResponse::Ok().body("Task deleted")
} else {
HttpResponse::NotFound().body("Task not found")
}
}
pub fn init_routes(app: &mut App) {
app.service(
web::resource("/tasks")
.route(web::get().to(index))
.route(web::post().to(create_task)),
)
.service(
web::resource("/tasks/{id}")
.route(web::get().to(get_task))
.route(web::put().to(update_task))
.route(web::delete().to(delete_task)),
);
}
#[derive(Deserialize)]
pub struct NewTask {
title: String,
}
#[derive(Deserialize)]
pub struct UpdateTask {
title: Option<String>,
completed: Option<bool>,
}
Étape 6 : Initialisation et démarrage du serveur
Modifier src/main.rs :
use actix_web::{web, App, HttpServer};
use std::sync::{Arc, Mutex};
use super::routes::init_routes;
use super::services::TaskService;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let task_service = TaskService::new();
let data = web::Data::new(Arc::new(Mutex::new(task_service)));
HttpServer::new(move || {
App::new()
.app_data(data.clone())
.service(init_routes)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Étape 7 : Tests locaux
Vous pouvez tester les API avec curl ou un outil comme Postman :
## Créer une tâche
curl -X POST http://127.0.0.1:8080/tasks -H "Content-Type: application/json" -d '{"title": "Learn Rust"}'
## Obtenir toutes les tâches
curl http://127.0.0.1:8080/tasks
## Mettre à jour une tâche
curl -X PUT http://127.0.0.1:8080/tasks/1 -H "Content-Type: application/json" -d '{"title": "Learn Rust", "completed": true}'
## Supprimer une tâche
curl -X DELETE http://127.0.0.1:8080/tasks/1
Erreurs frequentes et debugging
1. Erreur de sérialisation/désérialisation
// ❌ Mauvais
let user = serde_json::from_str::<User>(&json).unwrap();
// ✅ Correct
let user: User = serde_json::from_str(&json).map_err(|e| e.to_string())?;
2. Erreur de middleware
// ❌ Mauvais
app.service(
web::resource("/tasks/{id}")
.route(web::get().to(get_task))
.route(web::put().to(update_task))
.route(web::delete().to(delete_task)),
);
// ✅ Correct
app.service(
web::resource("/tasks/{id}")
.route(web::get().to(get_task))
.route(web::put().to(update_task))
.route(web::delete().to(delete_task)),
);
3. Erreur de gestion des erreurs
// ❌ Mauvais
if let Some(task) = data.get_task(&task_id.into_inner()) {
HttpResponse::Ok().json(task)
} else {
HttpResponse::NotFound().body("Task not found")
}
// ✅ Correct
if let Some(task) = data.get_task(&task_id.into_inner()) {
HttpResponse::Ok().json(task)
} else {
HttpResponse::NotFound().body("Task not found")
}
Pour aller plus loin
1. Gestion des erreurs avancée
Explorez les concepts de gestion des erreurs avec Rust, en utilisant les Result et Option pour gérer les cas d'erreur.
2. Authentification JWT
Ajoutez une authentification JWT à votre API pour sécuriser l'accès aux ressources.
3. Tests unitaires et d'intégration
Ajoutez des tests unitaires et d'intégration pour vous assurer que votre API fonctionne comme prévu.
Défi pratique
Défi : Gestionnaire de blog avec authentification JWT
Construire un gestionnaire de blog avec une authentification JWT. L'application doit permettre aux utilisateurs d'inscrire, se connecter et créer des articles.
- Utilisez
jsonwebtokenpour l'authentification. - Créez des endpoints pour les utilisateurs (
/users/register,/users/login) et les articles (/posts/create,/posts/list).
N'hésitez pas à tester votre application avec Postman ou des outils de test d'API.