ws-bridge 0.2.1

Strongly-typed WebSocket endpoints for Rust — define once, use everywhere (axum server, yew/gloo browser client, tokio-tungstenite native client)
Documentation

ws-bridge

Strongly-typed WebSocket endpoints for Rust — define once, use everywhere.

Server (axum) · Browser client (yew/gloo) · Native client (tokio-tungstenite)

Crates.io Docs.rs MIT licensed

The problem

WebSocket projects in Rust tend to accumulate duplicated boilerplate:

  • Path strings scattered across server routes and client connect calls
  • Serde plumbing (serde_json::to_string / from_str) at every send/recv
  • URL derivation (ws:// vs wss://) copy-pasted in every frontend
  • Split + send task pattern repeated in every axum handler
  • Reconnection logic reimplemented in every project

The solution

Define your endpoint once:

use ws_bridge::WsEndpoint;
use serde::{Serialize, Deserialize};

pub struct EchoEndpoint;

impl WsEndpoint for EchoEndpoint {
    const PATH: &'static str = "/ws/echo";
    type ServerMsg = ServerMsg;
    type ClientMsg = ClientMsg;
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ServerMsg {
    Welcome { message: String },
    Echo { payload: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ClientMsg {
    Say { text: String },
    Quit,
}

Use it everywhere with full type safety:

Server (axum)

use axum::Router;
use ws_bridge::WsEndpoint;

let app = Router::new().route(
    EchoEndpoint::PATH,
    ws_bridge::server::handler::<EchoEndpoint, _, _>(|mut conn| async move {
        conn.send(ServerMsg::Welcome { message: "Hello!".into() }).await.unwrap();
        while let Some(Ok(msg)) = conn.recv().await {
            // msg is ClientMsg — fully typed
        }
    }),
);

Native client (tokio-tungstenite)

let mut conn = ws_bridge::native_client::connect::<EchoEndpoint>("ws://localhost:3000").await?;
conn.send(ClientMsg::Say { text: "hello".into() }).await?;
while let Some(Ok(msg)) = conn.recv().await {
    // msg is ServerMsg — fully typed
}

Browser client (yew/gloo)

let mut conn = ws_bridge::yew_client::connect::<EchoEndpoint>()?;
conn.send(ClientMsg::Say { text: "hello".into() }).await?;
while let Some(Ok(msg)) = conn.recv().await {
    // msg is ServerMsg — fully typed
}

Features

Enable only what you need:

Feature What it provides Dependencies
server axum WebSocket handlers axum, tokio, futures-util
yew-client Browser WebSocket client (WASM) gloo-net, wasm-bindgen-futures, web-sys
native-client tokio-tungstenite client tokio-tungstenite, tokio, futures-util
reconnect Exponential backoff reconnection tokio, futures-util
[dependencies]
# Server crate
ws-bridge = { version = "0.1", features = ["server"] }

# Frontend crate (WASM)
ws-bridge = { version = "0.1", features = ["yew-client"] }

# CLI / daemon crate
ws-bridge = { version = "0.1", features = ["native-client", "reconnect"] }

Key concepts

  • WsEndpoint — A trait you implement on a unit struct. Defines PATH, ServerMsg, and ClientMsg. This is the single source of truth shared between server and clients.
  • WsCodec — Encode/decode trait. Any Serialize + DeserializeOwned type automatically gets JSON encoding. Implement manually for binary protocols.
  • WsConnection<S, R> — Transport-agnostic typed connection. send() only accepts S, recv() only yields R.
  • NoMessages — Uninhabited type for unidirectional endpoints (e.g., server-push-only streams).

Documentation

Examples

Run the examples with:

# Terminal 1: start the server
cargo run --example echo_server --features server

# Terminal 2: connect a client
cargo run --example echo_client --features native-client

# Server with shared state
cargo run --example stateful_server --features server

# Reconnecting client
cargo run --example reconnecting_client --features "native-client,reconnect"

License

MIT