ruststream 0.7.5

Async messaging framework for Rust: broker-agnostic traits, router, codecs, and a conformance harness for broker authors.
Documentation
---
description: "Build an orders service step by step: a message type, a handler, a reply and a router on the in-memory broker, then a real broker."
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Tutorial: build your first service

By the end of this page you have a running orders service: a message type, a handler, a reply, and a
router that collects them. It runs on the in-memory broker, so there is nothing external to start.
Swapping in a real broker is a one-line change, and step 7 shows it.

## 1. Create the crate

```bash
cargo new orders-service
cd orders-service
```

```toml title="Cargo.toml"
[package]
name = "orders-service"
version = "0.1.0"
edition = "2024"

[dependencies]
ruststream = { version = "0.7", features = ["macros", "memory", "json", "asyncapi", "logging"] }
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
```

## 2. Define a message and a handler

A handler is an `async fn` whose first parameter is the decoded payload. The `#[subscriber]` macro
turns it into a subscriber definition named after the function.

<Tabs groupId="api">
<TabItem value="macros" label="Macros">

```rust title="src/orders.rs" reference="examples/tutorial/orders.rs#order"
```

</TabItem>
<TabItem value="manual" label="Manual">

```rust title="src/orders.rs" reference="examples/manual/tutorial/orders.rs#order"
```

</TabItem>
</Tabs>

A handler returns a [`HandlerOutcome`](https://docs.rs/ruststream/latest/ruststream/runtime/index.html#subscribers): an `ack`, or a `nack` that
drops or requeues the message. You can return `()` or `Result<(), E>` instead, where `Ok` acks and
`Err` drops.

The `JsonSchema` derive puts the payload's schema into the AsyncAPI document of step 6. The type's
doc comment becomes the message description there. You need no extra dependency for it: the
`asyncapi` feature re-exports `schemars`.

## 3. Wire it into an app

<Tabs groupId="api">
<TabItem value="macros" label="Macros">

```rust title="src/main.rs" reference="examples/tutorial/first_app.rs#app"
```

</TabItem>
<TabItem value="manual" label="Manual">

```rust title="src/main.rs" reference="examples/manual/tutorial/first_app.rs#app"
```

</TabItem>
</Tabs>

:::tip[Codec defaults]

`include` decodes with the default codec, so it needs no codec argument. The default is `json`
when the feature is enabled, otherwise `cbor`, otherwise `msgpack`. You can set one codec for
all the broker's handlers at once with `with_broker_codec(broker, codec, |b| ...)`. See
[Codecs](https://docs.rs/ruststream/latest/ruststream/codec/index.html) for the full resolution rules.

:::

Run it:

```bash
cargo run -- run
```

The `logging` feature prints the service's log to the terminal: the startup lines, then a line
for each order the handler logs with `tracing::info!`.

## 4. Reply to messages

To publish a reply, return the reply value and write `reply` on the subscriber. The `Outgoing`
derive on the reply type says where it goes:

<Tabs groupId="api">
<TabItem value="macros" label="Macros">

```rust title="src/orders.rs" reference="examples/tutorial/orders.rs#confirm"
```

</TabItem>
<TabItem value="manual" label="Manual">

```rust title="src/orders.rs" reference="examples/manual/tutorial/orders.rs#confirm"
```

</TabItem>
</Tabs>

Mount `confirm` next to `handle` with the same `include`. The reply is published with the broker's
default publish policy and encoded with the default codec.

<Tabs groupId="api">
<TabItem value="macros" label="Macros">

```rust title="src/main.rs" reference="examples/tutorial/reply_app.rs#reply"
```

</TabItem>
<TabItem value="manual" label="Manual">

```rust title="src/main.rs" reference="examples/manual/tutorial/reply_app.rs#reply"
```

</TabItem>
</Tabs>

Publishing from inside a handler and the other ways to publish are in
[Publishing & replies](https://docs.rs/ruststream/latest/ruststream/runtime/index.html#publishing).

## 5. Organize with a router

As the number of handlers grows, keep them in their own module and collect them into a
[`Router`](https://docs.rs/ruststream/latest/ruststream/runtime/index.html#routing):

<Tabs groupId="api">
<TabItem value="macros" label="Macros">

```rust title="src/routes.rs" reference="examples/tutorial/routes.rs#routes"
```

</TabItem>
<TabItem value="manual" label="Manual">

```rust title="src/routes.rs" reference="examples/manual/tutorial/routes.rs#routes"
```

</TabItem>
</Tabs>

`include` adds a plain handler to the router directly. A handler that publishes a reply hands back
a mount chain instead: `.out_reply(..)` names the reply's publish policy, and `.build()` finishes
the registration. Without `.out_reply(..)`, `.build()` takes the broker's default publish policy -
the same one `include` took in step 4. [Routing](https://docs.rs/ruststream/latest/ruststream/runtime/index.html#routing) covers the rest of the router
surface.

<Tabs groupId="api">
<TabItem value="macros" label="Macros">

```rust title="src/main.rs" reference="examples/tutorial/main.rs#main"
```

</TabItem>
<TabItem value="manual" label="Manual">

```rust title="src/main.rs" reference="examples/manual/tutorial/main.rs#main"
```

</TabItem>
</Tabs>

## 6. Inspect the AsyncAPI document

```bash
cargo run -- asyncapi gen
```

Every subscriber adds a channel and a `receive` operation to the document. `handle` and `confirm`
share the `orders` channel and get one operation each, because their subscriptions are separate. The
reply adds a `send` operation on `confirmations`.

The document keeps the payload schemas under `components.messages`. The output flags (`-o`,
`--yaml`) and the document itself are covered in [AsyncAPI](https://docs.rs/ruststream/latest/ruststream/asyncapi/index.html).

## 7. Swap in a real broker

Nothing above is tied to the in-memory broker: the broker is chosen at `with_broker`, so the swap is
a one-line change. Add the broker crate as a dependency and construct it there instead of
`MemoryBroker::new()`, for example `NatsBroker::new("nats://localhost:4222")`. The handlers, the
router and the codecs stay as they are. [Brokers](../../brokers/index.md#switching-brokers) lists the
available brokers and the swap for each of them.

:::info[The complete service is a compiled example]

Every snippet on this page comes from
[`examples/tutorial`](https://github.com/powersemmi/ruststream/tree/main/examples/tutorial)
in the repository, which CI builds on every change. `first_app.rs` and `reply_app.rs` are the
service as steps 3 and 4 leave it, and `main.rs` is the finished one. Run it yourself with
`cargo run --example tutorial --features macros,memory,json,asyncapi,logging -- run`.

:::

## Next steps

- [Middleware]https://docs.rs/ruststream/latest/ruststream/runtime/index.html#middleware - cross-cutting logic around handlers.
- [Lifespan]https://docs.rs/ruststream/latest/ruststream/runtime/index.html#lifecycle - shared state and startup/shutdown hooks.
- [Testing]https://docs.rs/ruststream/latest/ruststream/testing/index.html - test the handlers you just wrote, in-process.
- [Metrics]https://docs.rs/ruststream/latest/ruststream/metrics/index.html - Prometheus counters and histograms.