Quick start: building an application

Updated Nov 29, 2025

A WireframeApp collects route handlers and middleware. Each handler is stored as an Arc pointing to an async function that receives a packet reference and returns (). The builder caches these registrations until handle_connection constructs the middleware chain for an accepted stream.[^2]

use std::sync::Arc;
use wireframe::app::{Envelope, Handler, WireframeApp};

async fn ping(_env: &Envelope) {}

fn build_app() -> wireframe::Result<WireframeApp> {
    let handler: Handler<Envelope> = Arc::new(|env: &Envelope| {
        let _ = env; // inspect payload here
        Box::pin(ping(env))
    });

    WireframeApp::new()?
        .route(1, handler)?
        .wrap(wireframe::middleware::from_fn(|req, next| async move {
            let mut response = next.call(req).await?;
            response.frame_mut().extend_from_slice(b" pong");
            Ok(response)
        }))
}

The snippet below wires the builder into a Tokio runtime, decodes inbound payloads, and emits a serialised response. It showcases the typical main function for a microservice that listens on localhost and responds to a Ping message with a Pong payload.[^2][^10][^15]

use std::{net::SocketAddr, sync::Arc};

use wireframe::{
    app::{Envelope, Handler, WireframeApp},
    middleware,
    message::Message,
    server::{ServerError, WireframeServer},
};

#[derive(bincode::Encode, bincode::BorrowDecode, Debug)]
struct Ping {
    body: String,
}

#[derive(bincode::Encode, bincode::BorrowDecode, Debug, PartialEq)]
struct Pong {
    body: String,
}

async fn ping(env: &Envelope) {
    log::info!("received correlation id: {:?}", env.clone().into_parts().correlation_id());
}

fn build_app() -> wireframe::app::Result<WireframeApp> {
    let handler: Handler<Envelope> = Arc::new(|env: &Envelope| Box::pin(ping(env)));

    WireframeApp::new()?
        .route(1, handler)?
        .wrap(middleware::from_fn(|req, next| async move {
            let ping = Ping::from_bytes(req.frame()).map(|(msg, _)| msg).ok();
            let mut response = next.call(req).await?;

            if let Some(ping) = ping {
                let payload = Pong {
                    body: format!("pong {}", ping.body),
                }
                .to_bytes()
                .expect("encode Pong message");
                response.frame_mut().clear();
                response.frame_mut().extend_from_slice(&payload);
            }

            Ok(response)
        }))
}

fn app_factory() -> WireframeApp {
    build_app().expect("configure Wireframe application")
}

#[tokio::main]
async fn main() -> Result<(), ServerError> {
    let addr: SocketAddr = "127.0.0.1:4000".parse().expect("valid socket address");
    let server = WireframeServer::new(app_factory).bind(addr)?;
    server.run().await
}

Route identifiers must be unique; the builder returns WireframeError::DuplicateRoute when you try to register a handler twice, keeping the dispatch table unambiguous.[^2][^5] New applications default to the bundled bincode serializer, a 1024-byte frame buffer, and a 100 ms read timeout. Clamp these limits with buffer_capacity and read_timeout_ms, or swap the serializer with with_serializer when you need a different encoding strategy.[^3][^4]

Once a stream is accepted—either from a manual accept loop or via WireframeServerhandle_connection(stream) builds (or reuses) the middleware chain, wraps the transport in a length-delimited codec, enforces per-frame read timeouts, and writes responses. Serialization helpers send_response and send_response_framed return typed SendError variants when encoding or I/O fails, and the connection closes after ten consecutive deserialization errors.[^6][^7]