Backend

Rust

reqwest and serde, with the client built once and cloned.

Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
src/pulse.rs
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::time::Duration;

const BASE: &str = "https://api.thepulseapi.com/api/v1";

#[derive(Serialize)]
pub struct Signal {
    #[serde(rename = "type")]
    pub kind: String,
    pub value: String,
}

#[derive(Clone)]
pub struct Pulse {
    // reqwest::Client is already an Arc around a connection pool, so cloning it is cheap
    // and sharing one across tasks is the intended use. Building one per request is not.
    http: reqwest::Client,
    org_id: String,
}

impl Pulse {
    pub fn new(api_key: &str, org_id: impl Into<String>) -> reqwest::Result<Self> {
        let mut headers = reqwest::header::HeaderMap::new();
        let mut key = reqwest::header::HeaderValue::from_str(api_key).expect("valid api key");
        key.set_sensitive(true);
        headers.insert("x-api-key", key);

        Ok(Self {
            http: reqwest::Client::builder()
                .default_headers(headers)
                .timeout(Duration::from_secs(5))
                .build()?,
            org_id: org_id.into(),
        })
    }

    async fn post(&self, path: &str, mut body: Value) -> reqwest::Result<Value> {
        // organizationId is required on nearly every endpoint and comes from the body,
        // not from the key.
        body["organizationId"] = json!(self.org_id);

        self.http
            .post(format!("{BASE}{path}"))
            .json(&body)
            .send()
            .await?
            .error_for_status()?
            .json()
            .await
    }

    pub async fn track(&self, session_id: &str, signals: Vec<Signal>) -> reqwest::Result<Value> {
        self.post("/track", json!({ "sessionId": session_id, "signals": signals })).await
    }

    pub async fn ranked_catalog(&self, session_id: &str, limit: u32) -> reqwest::Result<Value> {
        self.post("/catalog/ranked", json!({ "sessionId": session_id, "limit": limit })).await
    }
}
let pulse = Pulse::new(&std::env::var("PULSE_API_KEY")?, std::env::var("PULSE_ORG_ID")?)?;

// Ranking is an enhancement. Fall back to catalogue order rather than failing the response.
let ranked = pulse.ranked_catalog(&session_id, 24).await.unwrap_or_else(|_| json!({ "items": [] }));

set_sensitive on the key header

It keeps the value out of Debug output for the header map, which is the kind of log line that ends up in an error report without anyone deciding to put it there.