lilydocs
HTTP APIWebSocketConsumer
Getting started3 min read

Publish your first message

Send a canonical Lily message from a separate publisher and interpret broker confirms correctly.

To see the Consumer process a message, you need a producer using the same contract. This page creates a separate Cargo application that publishes JSON to the orders exchange with the orders.created routing key.

Prepare topology first#

Start your first Consumer and wait for queue registration to complete. Neither QueueClientService initialization nor publish_json creates exchanges, queues, or bindings. The orders exchange and orders.created binding must exist when publishing.

If deployment requires the publisher to create topology, use the explicit RabbitMqTopologyBootstrap::from_client(...).run(...) operational step; named publishers use from_cell. The topology guide covers that separate decision. This workflow keeps topology ownership with the Consumer.

Prepare the publisher project#

Terminal
cargo new orders-publisher
cd orders-publisher
TOML
[package]
name = "orders-publisher"
version = "0.1.0"
edition = "2024"
rust-version = "1.96.1"

[dependencies]
lilyrs = { version = "0.1.0", features = ["queue-client", "injection"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

This process uses queue-client for publishing and injection because it constructs its own container. Keep the Consumer project's manifest unchanged: the two applications have separate feature profiles.

Download publisher.toml below and place it at the publisher project's root. It is separate from the Consumer's lily.toml:

publisher.toml
[queue_client]
mode = "single"
hostname = "127.0.0.1"
port = 5672
username = "guest"
password = "guest"
vhost = "/"
use_tls = false
pool_size = 1
persistence_enabled = true

Publish the message#

Save the complete publisher file as src/main.rs:

publisher.rs
use lilyrs::{
    injection::ApplicationContainer,
    queue_client::{PublishSchemaVersion, QueueClientService},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let container = ApplicationContainer::build().await?;
    let publish = async {
        let publisher = container.resolve::<QueueClientService>(None).await?;
        publisher.publish_json(
            "orders", "orders.created", PublishSchemaVersion::V1,
            serde_json::json!({"order_id": "order-42"}),
        ).await?;
        Ok::<(), Box<dyn std::error::Error>>(())
    }.await;
    let cleanup = container.close().await;
    // Always await container cleanup, including after publish failure.
    if let Err(error) = &cleanup { eprintln!("Publisher cleanup failed: {error}"); }
    publish?;
    cleanup?;
    Ok(())
}

PublishSchemaVersion::V1 is the application message version. publish_json sets body encoding, the json content kind, and the JSON MIME type together, and creates a fresh canonical event ID. The first handler accepts the same queue/version/content contract.

Terminal
LILY_CONFIG_PATH=./publisher.toml LILY_CONFIG_MODE=development cargo run

On successful delivery, the Consumer terminal prints order-42. A publisher Ok result means the mandatory message was not returned as unroutable and a broker confirm ACK was received. It does not establish that the handler completed or a database transaction committed.

Manage resources and repeated publication#

The example awaits container cleanup even when publication fails, and reports cleanup errors. If you resolve a publisher from an HTTP or WebSocket handler, keep container shutdown with the host that owns it.

Use a fresh event ID for a new business event. When resending the same event, retain its identity through PublishMetadata::try_new(stored_id, version) and a publish method that accepts metadata. Generating a new ID on every attempt breaks the deduplication chain. Transactional outbox provides a separate model for durable database-backed publication.

Sources for this page v0.1.0

This guide is based on the reviewed framework commit.

QUICK START
Documentation
Source code