Your first WebSocket application
Open a namespace, receive typed messages, and return an event or ACK to the calling client.
On this page, we will build a server that responds to hello:greet with a hello:greeted event. A second action, hello:confirm, will return the incoming data in a correlated ACK.
Prepare the project#
This guide uses the lilyrs 0.1.0 facade published on crates.io. The manifest's Rust minimum and the project's validated toolchain are 1.96.1, with edition 2024. No framework checkout is required. Create a project:
cargo new hello-lily-ws[package]
name = "hello-lily-ws"
version = "0.1.0"
edition = "2024"
rust-version = "1.96.1"
[dependencies]
lilyrs = { version = "0.1.0", features = ["websocket"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }The websocket feature enables lilyrs::websocket. lilyrs enables no components by default; feature names may use hyphens while module paths use underscores. Controller macros, async_trait, and Injectable come from the same framework root; your own DI services need no separate registry/derive dependencies. Tokio remains a direct dependency because this example uses #[tokio::main].
When adding an integration, extend the feature list on the same lilyrs dependency. postgresql, mongodb, redis, and websocket-client select single mode; for a factory, select the corresponding *-factory feature instead of the base feature. Cargo unifies features, so do not enable both in one dependency graph or use --all-features. The external client's manual ownership profile is covered separately in the client guide.
Controller and server#
Save the following file as src/main.rs:
use std::sync::Arc;
use lilyrs::websocket::{
Ack, Emit, Extensions, Payload, ServerConfig, WebSocketActionError,
WebSocketController, WebSocketControllerInitError, WebSocketControllerTrait,
WsAppBuilder, async_trait, websocket_controller,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
struct Greeting { name: String }
#[derive(WebSocketController)]
#[namespace("hello")]
struct HelloController;
#[async_trait]
impl WebSocketControllerTrait for HelloController {
async fn new(_: Arc<Extensions>) -> Result<Self, WebSocketControllerInitError> {
Ok(Self)
}
}
#[websocket_controller]
impl HelloController {
#[message("greet")]
async fn greet(&self, Payload(input): Payload<Greeting>)
-> Result<Emit<String>, WebSocketActionError>
{
Emit::new("hello:greeted", format!("Merhaba, {}!", input.name))
.map_err(WebSocketActionError::internal)
}
#[message("confirm")]
async fn confirm(&self, Payload(input): Payload<Greeting>)
-> Result<Ack<Greeting>, WebSocketActionError>
{
Ok(Ack::new(input))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = WsAppBuilder::new("127.0.0.1:8081")
.config(ServerConfig {
allowed_origins: vec!["http://localhost:3000".into()],
require_subprotocol: true,
..ServerConfig::default()
})
.build().await?;
app.start().await?;
Ok(())
}Download the complete example.
#[namespace("hello")] identifies the connection's namespace, and #[message("greet")] identifies the action. Together they form the hello:greet route. Controllers are discovered at link time; no separate add_controller call is needed. If you move the example to another module, include that module in the application with a mod declaration.
Run and connect#
cd hello-lily-ws
cargo runThe server accepts connections on 127.0.0.1:8081 at /ws?namespace=hello. This example allows only the http://localhost:3000 Origin and requires the lily.v2 subprotocol. Run the first client code from that Origin. 127.0.0.1:3000 is not the same Origin as localhost:3000.
Emit produces an event only for the calling connection. Later, we will use client targets to broadcast to other clients. For Ack, the client must send an ack_id; the server does not create a missing ID.
Shut down#
Pressing Ctrl+C in the terminal initiates shutdown. start().await returns the application's shutdown result. If a successful build has not yet been followed by start, close resources with app.close().await. Dropping the value does not prove that async cleanup has completed.
This first application was verified over a real loopback connection for events, ACKs, missing ACK IDs, and shutdown. See testing for the test setup.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.