Skip to main content

Getting started

This guide installs ruststream-nats, starts a local NATS server and runs a first service against it.

Installation​

Cargo.toml
[dependencies]
ruststream = { version = "0.7", features = ["macros", "json", "logging"] }
ruststream-nats = "0.7"
serde = { version = "1", features = ["derive"] }
tracing = "0.1"

[dev-dependencies]
ruststream-nats = { version = "0.7", features = ["testing"] }
tokio = { version = "1", features = ["macros", "rt"] }

ruststream is the framework: macros gives the attributes, json the default codec, and logging prints the service's log to the terminal. ruststream-nats is the NATS broker. Its testing feature adds the in-process NATS that tests run on.

The broker crate has two features, both off by default:

FeatureWhat it gives
testingthe in-process NATS behind TestApp
asyncapithe NATS bindings in the generated AsyncAPI document

The client underneath is async-nats, written in Rust with TLS through rustls, so the build needs no system libraries.

A local NATS​

One NATS server with JetStream is enough to develop against:

compose.yaml
services:
nats:
image: nats:2-alpine
command: ["-js"]
ports:
- "4222:4222"
docker compose up -d

The server listens on localhost:4222. -js turns on JetStream, the streams that the guides from Streams and consumers on read and write. The examples publish with the NATS command-line tool, nats.

The first service​

src/main.rs
#[derive(Debug, PartialEq, Deserialize)]
struct Order {
id: u64,
amount_cents: u64,
}

#[subscriber("orders.created")]
async fn take_order(order: &Order) -> HandlerOutcome {
if order.amount_cents == 0 {
tracing::warn!(order.id, "order without an amount dropped");
return HandlerOutcome::drop();
}
tracing::info!(order.id, order.amount_cents, "order taken");
HandlerOutcome::ack()
}

#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
NatsBroker::new("nats://localhost:4222"),
|b| {
b.include(take_order);
},
)
}

take_order reads JSON orders from the Core NATS subject orders.created. It takes an order with an amount and drops one without. NatsBroker::new takes the server address and only records it. The connection opens when the service starts. #[ruststream::app] turns app into main; in the manual form, run_main does the same.

Run the service:

cargo run -- run

Publish an order from another terminal:

nats pub orders.created '{"id":1,"amount_cents":1250}'

The service logs order taken with the order's id. Core NATS has no acknowledgement, so the log also shows ack / nack failed for each order: the runtime reports that the delivery could not be settled. Acknowledgement and delivery guarantees explains when that matters and how a stream settles each message. Ctrl+C stops the service.

cargo generate scaffolds the same kind of service with a router:

cargo generate --git https://github.com/powersemmi/ruststream-nats templates/nats --name my-service

templates/nats-js scaffolds a service that reads a stream through a durable consumer.

The first test​

The example file also holds two tests. Each starts the same app with TestApp::start, publishes an order and checks how take_order settled it. NatsBroker connects to an in-process NATS there, so the tests need no server:

cargo test

Testing a NATS service walks through such tests and what the in-process NATS models.

Coming from async-nats​

A service on async-nats writes its own subscription loops and publishes by hand. Here is where each part goes in RustStream:

async-natsRustStream
async_nats::connect or ConnectOptions::connectNatsBroker::new, with with_options for the ConnectOptions
client.subscribe and a while let Some(message) loopa #[subscriber("subject")] handler
client.queue_subscribeCoreSubject::queue_group (Subjects, wildcards and queue groups)
serde_json::from_slice(&message.payload)the handler's parameter type, decoded by the codec
client.publisha reply or an Out parameter, sent through the Publish policy
client.requestrequest on an Out<impl RequestReply, _> slot (Request-reply)
create_consumer on a stream and a messages() loopJetStreamSubject (Streams and consumers)
message.ack(), AckKind::Nak, AckKind::Termthe handler's outcome: ack, retry, retry_after, drop (Acknowledgement and delivery guarantees)
jetstream.publish and its acknowledgementJetStreamPublish (Publishing to streams)