ESC
Type to search...

Rapina 0.9.0

Real-time with WebSockets and Relay, OpenAPI import, starter templates, and an interactive tutorial

Rapina 0.9.0 shipped on March 6, 2026. This was the largest release to date, centered around real-time features — raw WebSocket support and a higher-level Relay system for topic-based push. It also added OpenAPI import, starter templates, and an interactive tutorial. The project moved to the rapina-rs GitHub org in this release.


WebSocket support

Raw WebSocket support was added behind the websocket feature flag. The WebSocketUpgrade extractor handled the HTTP upgrade handshake; WebSocket gave direct access to the bidirectional connection.

Enable the feature:

[dependencies]
rapina = { version = "0.9", features = ["websocket"] }

A simple echo handler:

use rapina::prelude::*;
use rapina::websocket::{WebSocketUpgrade, Message};

#[get("/ws")]
#[public]
async fn ws(upgrade: WebSocketUpgrade) -> impl IntoResponse {
    upgrade.on_upgrade(|mut socket| async move {
        while let Some(Ok(msg)) = socket.recv().await {
            if msg.is_text() || msg.is_binary() {
                socket.send(msg).await.ok();
            }
        }
    })
}

on_upgrade spawned a task for the connection and returned the 101 Switching Protocols response immediately.


Relay system

The Relay system added topic-based push over WebSocket without requiring handlers to manage connections directly. Any handler could push a message to a topic; connected clients received it through a shared WebSocket endpoint.

Setup

use rapina::relay::RelayConfig;
use rapina::prelude::*;

Rapina::new()
    .with_relay(RelayConfig::default())
    .discover()
    .listen("127.0.0.1:3000")
    .await

RelayConfig::default() registered a WebSocket endpoint at /ws with a broadcast buffer of 128 messages per topic and a maximum of 50 subscriptions per connection.

Pushing from handlers

The Relay extractor was available in any handler. relay.push() delivered a message to all clients subscribed to the given topic:

use rapina::relay::Relay;
use rapina::prelude::*;

#[post("/orders")]
async fn create_order(relay: Relay, body: Json<NewOrder>) -> Result<Json<Order>> {
    let order = save_order(&body).await?;
    relay.push("orders:new", "created", &order).await?;
    Ok(Json(order))
}

Channel handlers

The #[relay("pattern")] attribute registered a server-side handler that received RelayEvent callbacks for subscribe, message, and disconnect events. Patterns supported exact matches and prefix wildcards:

use rapina::relay::{Relay, RelayEvent};
use rapina::prelude::*;

#[relay("room:*")]
async fn room_channel(event: RelayEvent, relay: Relay) -> Result<()> {
    match &event {
        RelayEvent::Join { topic, conn_id } => {
            relay.track(topic, *conn_id, serde_json::json!({}));
        }
        RelayEvent::Message { topic, event: ev, payload, .. } => {
            relay.push(topic, ev, payload).await?;
        }
        RelayEvent::Leave { topic, conn_id } => {
            relay.untrack(topic, *conn_id);
        }
    }
    Ok(())
}

"room:*" matched any topic starting with "room:". Exact patterns took priority over prefix patterns during dispatch.

Presence tracking

relay.track() and relay.untrack() maintained a presence map per topic. relay.presence(topic) returned the current list of tracked connections with their associated metadata.


rapina import openapi

The rapina import openapi command scaffolded handlers, request/response types, and routes from an existing OpenAPI spec. It supported JSON and YAML files and required the import-openapi feature:

# Generate handlers from a spec file
rapina import openapi api.yaml

# Preview without writing files
rapina import openapi api.yaml --dry-run

# Filter by tag
rapina import openapi api.yaml --tags users,orders

Starter templates

rapina new gained a --template flag with three options:

# Default REST API scaffold (no flag needed)
rapina new my-app

# CRUD template — resource model, SeaORM entity, full handler set
rapina new my-app --template crud

# Auth template — JWT login, protected routes, CurrentUser extractor wired up
rapina new my-app --template auth

Each template generated a fully working project with Cargo.toml, migrations, and example handlers.


group parameter in route macros

Route macros gained a group parameter for prefixing routes during auto-discovery. Routes registered via discover() were grouped under the specified prefix without changing their handler path:

#[get("/users", group = "/api/v1")]
async fn list_users() -> Json<Vec<User>> { ... }

#[get("/users/:id", group = "/api/v1")]
async fn get_user(id: Path<u64>) -> Json<User> { ... }

The above registered GET /api/v1/users and GET /api/v1/users/:id.


Interactive tutorial

A six-chapter interactive tutorial was added to the docs site. It covered routing, extractors, database integration, authentication, testing, and deployment, walking through a complete application from scratch.


Bug fixes and improvements

Graceful shutdown on Windows (#304)

Graceful shutdown previously relied on Unix signal handling and did not work on Windows. This was fixed by adding a Windows-compatible shutdown path using the windows crate for Ctrl+C interception alongside the existing Unix SIGTERM/SIGINT handling.

OpenAPI commands accept --host and --port

rapina openapi export, rapina openapi check, and rapina openapi diff gained --host and --port flags. Previously the commands always fetched the spec from http://127.0.0.1:3000. Now they can target any address:

rapina openapi export --host 0.0.0.0 --port 8080

Docs

New documentation pages were added for WebSocket, Relay, OpenAPI, Testing, Validation, and Migrations.


rapina-rs org

All repository references were updated to the new rapina-rs GitHub organization.


Upgrade by bumping the version in your Cargo.toml:

rapina = "0.9"