Introduction

Poem est un socle d’applications (un framework) qui permet l’écriture d’API WEB. Il prévoit tous les outils dont on pourrait avoir besoin, ainsi que la possibilité d’apporter les nôtres. Ainsi on ne s’embêtera pas à gérer nous-même, le routage, la sérialisation, la dé-sérialisation, la validation, le CORS, les Cookies , les mesures anti-CSRF, etc.

Requête simple

Les fonctions qui gèrent les requêtes s’appellent des handlers (gérer == to handle). Dans son plus simple appareil un handler ressemble à ceci :

#[poem::handler]
fn index() -> &'static str {
    "hello"
}

Le handler peut être monté a un endpoint (point-d’accès) comme ceci dans un serveur :

use poem::{Route, Server, listener::TcpListener, web, get};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    web_server_start().await;
    Ok(())
}

#[tokio::main]
async fn web_server_start() -> anyhow::Result<()> {
	let routes = Route::new()
		.at("/", get(index));
	Server::new(TcpListener::bind("0.0.0.0:3000")).run(routes).await?;
    Ok(())
}

Lancez le programme et faites une requête sur le port 3000, vous devriez voir quelque chose de similaire :

> http :3000
HTTP/1.1 200 OK
content-length: 5
content-type: text/plain; charset=utf-8
date: Tue, 25 Feb 2025 15:47:40 GMT

hello

Recevoir un fragment du chemin en paramètre

Contrairement au NodeJS, nous n’allons pas (ou rarement) traiter un objet request ou response dans nos handlers. À la place on va juste écrire les paramètre dont on a besoin, avec des types spécifiques à poem en fonction de ce dont notre handler a besoin pour travailler. On les appelle des extracteurs, la liste des extracteurs disponibles par défaut se trouve ici

Pour extraire des fragments du chemin, on va utiliser un paramètre du type poem::web::Path avec en paramètre générique :

  • le type de l’élément que l’on veut recevoir, s’il est tout seul ;
  • un tuple avec les types de tous les éléments que l’on veut recevoir (dans l’ordre), s’ils sont plusieurs.

Les valeurs contenues dans le paramètre Path peuvent être extraites en le déconstruisant :

  • en paramètre : poem::web::Path(a, b, c): poem::web::Path<(String, i32, String)> :
    • a, b et c deviennent des paramètres utilisables dans la fonction comme des variables ;
  • en assignation : let poem::web::Path(a, b, c) = path :
    • path provient d’un paramètre path: poem::web::Path<(String, i32, String)>,
    • a, b et c deviennent des variables utilisables dans la fonction.

Exemples :

use poem::{Route, Server, get, listener::TcpListener, web::Path};

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let routes = Route::new()
        .at("/:name", get(hello_name))
        .at("/alt/:name", get(hello_name_alt))
        .at("/:name/:year", get(hello_name_class));
    Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(routes)
        .await
}

#[poem::handler]
fn hello_name(Path(name): Path<String>) -> String {
    format!("hello {}", name)
}

#[poem::handler]
fn hello_name_alt(path: Path<String>) -> String {
    let Path(name) = path;
    format!("hello {}", name)
}

#[poem::handler]
fn hello_name_class(Path((name, year)): Path<(String, i32)>) -> String {
    format!("hello {} student in year {}", name, year)
}

Renvoi de Json

Pour qu’une requête réponde quelque chose, il suffit de faire renvoyer ce quelque chose à un de nos handlers. Pour le moment on a renvoyé des chaînes de caractères. La liste complète de ce que l’on peut renvoyer par défaut se trouve ici

Si on le souhaite, on peut construire une réponse nous-même, et/ou implémenter IntoResponse nous même, sur une des nos structures. La plupart du temps on utilisera poem::Json pour renvoyer du JSON, dont la sérialisation se fait avec serde_json.

Pour que cela fonctionne vous devez ajouter au projet :

[dependencies]
serde = { version = "1.0.218", features = ["derive"] }
serde_json = "1.0.139"
use poem::web::Json;
#[poem::handler]
fn random_student() -> Json<Student> {
    Json(Student {
        name: "Toto".to_string(),
        stream: Stream::CYB,
        year: 1,
    })
}

#[derive(serde::Serialize)]
enum Stream {
    MKT,
    CYB,
    PRG,
    AIB,
}

#[derive(serde::Serialize)]
struct Student {
    name: String,
    stream: Stream,
    year: u8,
}

Recevoir du JSON

En appliquant la logique des extracteurs à poem::web::Json on peut recevoir un objet JSON depuis le corps d’une requête et remplir une structure avec ses données. La structure doit #[derive(serde::Deserialize)] pour que cela fonctionne.

La fonction pass_student_through vous donne un exemple de handler qui reçois un Student extrait depuis le corps de la requête, le modifie, puis le renvoie.

use poem::{Route, Server, get, listener::TcpListener, web::Path};

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let routes = Route::new()
        .at("/student", get(random_student).post(pass_student_through));
    Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(routes)
        .await
}

use poem::web::Json;
// [handler random_student]

#[poem::handler]
fn pass_student_through(Json(mut student): Json<Student>) -> Json<Student> {
    student.name = student.name.to_uppercase();
    student.year += 1;
    Json(student)
}

#[derive(serde::Serialize, serde::Deserialize)]
enum Stream {
    MKT,
    CYB,
    PRG,
    AIB,
}

#[derive(serde::Serialize, serde::Deserialize)]
struct Student {
    name: String,
    stream: Stream,
    year: u8,
}

Exemple de requête avec HTTPie :

> http :3000/student name=toto stream=CYB year:=2
HTTP/1.1 200 OK
content-length: 39
content-type: application/json; charset=utf-8
date: Thu, 27 Feb 2025 15:17:05 GMT

{
    "name": "TOTO",
    "stream": "CYB",
    "year": 3
}

Recevoir un contexte ?

Quand on travaille avec une API, il y a souvent des données que l’on veut utiliser dans, voir modifier depuis, nos handlers. Ça peut être une connexion a une base de données, de la configuration, un service métier, etc.

Depuis un handler on peut recevoir des données ne provenant pas de la requête en écrivant :

#[derive(Clone)]
struct ExampleConfig {
    mode: String,
}

use poem::web::Data;

#[poem::handler]
fn example_with_context(Data(context): Data<&ExampleConfig>) -> String {
    format!("mode {}", context.mode)
}

La donnée doit être passée au routeur avec .data() (rendu disponible par poem::EndpointExt).

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let conf = ExampleConfig {
        mode: "dev config".to_string(),
    };
	
    use poem::{EndpointExt, get};
    let routes = poem::Route::new()
        .at("/ctx", get(example_with_context))
        .data(conf);

    poem::Server::new(poem::listener::TcpListener::bind("0.0.0.0:3000"))
        .run(routes)
        .await
}

poem sait quelles données envoyer au handler en fonction du type demandé. Ainsi vous ne pouvez pas différencier deux contextes du même type.

Modifier un contexte, exclusion mutuelle

Mettons vous voulez modifier un contexte, pour suivre le nombre de visite de votre API par exemple. Sauf que le contexte reçu ne peut pas être modifié, du fait d’être potentiellement partagé par plusieurs fils d’exécution.

Pour pouvoir modifier un contexte on doit utiliser des concepts qui s’appellent :

  • le comptage de références (reference counting) ;
  • la mutabilité interne (internal mutability) (comme avec RefCell) ; et
  • la synchronisation (comme avec des Mutex ou des variables atomiques).

Exemple avec un AtomicUsize :

use std::sync::Arc;
use std::sync::atomic::AtomicUsize;

struct ExampleCtx {
    mode: String,
    visits: AtomicUsize,
}

#[derive(Clone)]
struct ExampleConfig {
    mode: String,
}

use poem::web::Data;

#[poem::handler]
fn example_with_context(Data(context): Data<&ExampleConfig>) -> String {
    format!("mode {}", context.mode)
}

#[poem::handler]
fn example_with_context_and_visits(Data(context): Data<&Arc<ExampleCtx>>) -> String {
    use std::sync::atomic::Ordering::SeqCst;
    context.visits.fetch_add(1, SeqCst);
    let visits = context.visits.load(SeqCst);
    format!("mode {} with visits: {}", context.mode, visits)
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let conf = ExampleConfig {
        mode: "dev config".to_string(),
    };
    let context = Arc::new(ExampleCtx {
        mode: "dev context".to_string(),
        visits: AtomicUsize::new(0),
    });

    use poem::{EndpointExt, get};
    let routes = poem::Route::new()
        .at("/ctx", get(example_with_context))
        .at("/vis", get(example_with_context_and_visits))
        .data(context)
        .data(conf);

    poem::Server::new(poem::listener::TcpListener::bind("0.0.0.0:3000"))
        .run(routes)
        .await
}

Si la donnée que l’on veut stocker et modifier est plus complexe qu’un nombre, on va plutôt la ranger dans :

Traçabilité

Dans TD Instrumentalisation tracing nous avons vu l’avantage des portées (spans) pour contextualiser les événements de log émis dans le programme. Poem supporte la création de portées spécifiques à chacune de requête. Ainsi : on sait toujours pour quelle requête un log à été émis.

poem::middleware::Tracing ajoute le contexte de la requête aux événements produits pour elle : IP, méthode HTTP, URI.

poem::middleware::RequestId ajoute un identifiant alphanumérique à chaque requête, pour la rendre plus facilement… identifiable. L’identifiant est visible dans les logs ET en réponse de la requête, sur un header x-request-id qu’un utilisateur peut vous donner s’il a un problème.

Exemple de main.rs qui :

  • configure tracing_subscriber avec le log en json dans un fichier logs/app.jsonl ;
  • configure tracing_subscriber avec le log en pretty sur la sortie standard ;
  • démarre un server poem avec les middlewares de Tracing et de RequestId.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let _json_logs_guard = tracing_setup()?;
    web_server_start().await?;
    Ok(())
}

fn tracing_setup() -> anyhow::Result<tracing_appender::non_blocking::WorkerGuard> {
    std::fs::create_dir_all("logs")?;

    let json_file = tracing_appender::rolling::never("logs", "app.jsonl");
    let (json_writer, json_guard) = tracing_appender::non_blocking(json_file);

    let stdout_layer = tracing_subscriber::fmt::layer()
        .with_writer(std::io::stdout)
        .with_target(false)
        .with_ansi(true)
        .pretty();
    let json_layer = tracing_subscriber::fmt::layer()
        .with_writer(json_writer)
        .with_ansi(false)
        .with_target(true)
        .json();

    tracing_subscriber::registry()
        .with(stdout_layer)
        .with(json_layer)
        .with(tracing_subscriber::filter::LevelFilter::TRACE)
        .init();

    Ok(json_guard)
}

pub async fn web_server_start(db: sqlx::PgPool) -> anyhow::Result<()> {
    let api_routes = poem::Route::new()
        .at("/", get(index))
        .with(poem::middleware::Tracing::default())
        .to_response() // on ajoute RequestId après un `to_response` autrement le x-request-id n'est pas ajouter aux requêtes qui ont une erreur (ça serait dommage)
        .with(poem::middleware::RequestId::default());
    info!("starting server");
    poem::Server::new(poem::listener::TcpListener::bind("0.0.0.0:3000"))
        .run(api_routes)
        .await?;
    Ok(())
}

Exemple de requête :

● http :3001/password_auth/login email_addr=elise@ecole-89.com clear_password=ab
HTTP/1.1 401 Unauthorized
content-length: 22
content-type: text/plain; charset=utf-8
date: Mon, 30 Mar 2026 08:05:45 GMT
x-request-id: d49a920b-731c-4fb5-8300-d4ff37001004

verifying the password

Et les logs qu’elle peut produire :

2026-03-30T08:01:45.778861Z  INFO hecate::db: return: PasswordAuth {
   pwauth_email: "elise@ecole-89.com",
   pwauth_hash: "$argon2id$v=19$m=19456,t=2,p=...", profile_id: Some(2)
  }
    at src/db.rs:68
    in hecate::db::password_auth_get with email: "elise@ecole-89.com"
    in poem::middleware::tracing_mw::request with remote_addr: 127.0.0.1, version: HTTP/1.1, method: POST, uri: /password_auth/login, request_id: d49a920b-731c-4fb5-8300-d4ff37001004
    in poem::middleware::requestid:: with request_id: d49a920b-731c-4fb5-8300-d4ff37001004

2026-03-30T08:01:46.519364Z  WARN hecate::web_server: err: Password
    at src/web_server.rs:203
    in poem::middleware::tracing_mw::request with remote_addr: 127.0.0.1, version: HTTP/1.1, method: POST, uri: /password_auth/login, request_id: d49a920b-731c-4fb5-8300-d4ff37001004
    in poem::middleware::requestid:: with request_id: d49a920b-731c-4fb5-8300-d4ff37001004

2026-03-30T08:01:46.519574Z  INFO poem::middleware::tracing_mw: error, status: 401 Unauthorized, error: verifying the password, duration: 743.133731ms
    at /home/eriizu/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/poem-3.1.12/src/middleware/tracing_mw.rs:99
    in poem::middleware::tracing_mw::request with remote_addr: 127.0.0.1, version: HTTP/1.1, method: POST, uri: /password_auth/login, request_id: d49a920b-731c-4fb5-8300-d4ff37001004
    in poem::middleware::requestid:: with request_id: d49a920b-731c-4fb5-8300-d4ff37001004

Et enfin, un petit script pour que je vous donne en cadeau, qui peut :

  • vous lister les requêtes qui ont eu lieu dans vos logs ;
  • vous donner les événements qui ont eu lieu lors d’une requête.

Exemple d’utilisation :

cat logs/app.jsonl | sh ./logs_by_request_id.sh
cat logs/app.jsonl | sh ./logs_by_request_id.sh d49a920b-731c-4fb5-8300-d4ff37001004