Nouveau : Datasets open source gratuits disponibles !Decouvrir →
🦀
Intermediaire 30 min Rust

API REST avec Rust

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 --version dans votre terminal.
    • Serveur HTTP local : pour tester rapidement l'API, vous pouvez utiliser hyperlocal ou warp.

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 jsonwebtoken pour 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.

Besoin d'aide sur Rust ?

Besoin d'aide sur un projet technique ? Decrivez-le pour des conseils personnalises.

Recevoir des conseils

Questions frequentes

Quelle est la différence entre les méthodes GET et POST dans une API REST avec Rust?
La méthode GET est utilisée pour récupérer des données d'un serveur, tandis que la méthode POST est utilisée pour envoyer des données au serveur pour qu'il les traite ou les stocke.
Comment gérer les erreurs dans une API REST avec Rust?
Les erreurs peuvent être gérées en utilisant le système de résultats (Result) et des structures d'erreur personnalisées. On peut également utiliser des bibliothèques comme `anyhow` pour simplifier la gestion des erreurs.
Quelle est l'utilisation du trait `serde` dans les API REST avec Rust?
Serde est une bibliothèque populaire en Rust utilisée pour sérialiser et désérialiser les données. Elle permet de convertir facilement des structures de données Rust en format JSON ou XML, ce qui est essentiel pour les communications réseau dans les API REST.

Pages liees

Chaque semaine, le meilleur de la tech francaise

Tendances, salaires, outils et opportunites — directement dans votre boite mail.

Gratuit. Desabonnement en un clic. Pas de spam.