11 Getting Started
trev edited this page 2026-07-12 19:35:14 -04:00

Getting Started

This guide shows how to use TrevRPC in an application. It follows the same high-level shape as ConnectRPC: define a protobuf service, generate typed bindings, implement the service, register it on a server, and call it with a generated client.

TrevRPC is not a Connect or gRPC wire-compatible runtime. It uses a custom protobuf protocol over native QUIC, ordinary HTTP/3, or WebTransport.

Requirements

  • Protobuf definitions for your service.
  • The TrevRPC runtime for your language.
  • The matching TrevRPC generator for your language.
  • TLS and QUIC configuration for native trevrpc/1, ordinary HTTP/3, or WebTransport.

Use Local Development instead if you want to work on TrevRPC itself or run the checked-in examples.

1. Define a Service

Start with a normal protobuf service definition:

syntax = "proto3";

package example.greeter;

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloReply);
  rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
  rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
  rpc BidiHello(stream HelloRequest) returns (stream HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

TrevRPC supports unary, server-streaming, client-streaming, and bidirectional-streaming methods.

2. Generate TrevRPC Bindings

Install and run the matching generator for your language. With Buf, a minimal generation config looks like this:

version: v2
plugins:
  - local: protoc-gen-trevrpc-rust
    out: gen/rust
    opt:
      - runtime_path=::trevrpc
  - local: protoc-gen-trevrpc-go
    out: gen/go
    opt:
      - runtime_import=trev.zip/llc/trevrpc/trevrpc-go
  - local: protoc-gen-trevrpc-cpp
    out: gen/cpp
  - local: protoc-gen-trevrpc-kotlin
    out: gen/kotlin
    opt:
      - runtime_package=zip.trev.trevrpc
  - local: protoc-gen-trevrpc-js
    out: gen/js
    opt:
      - runtime_import=trevrpc-js

Generated code provides:

  • A service trait or interface to implement for Rust, Go, and Kotlin servers.
  • A typed client.
  • A Rust, Go, or Kotlin registration function that connects your implementation to the TrevRPC server.
  • C++20 {Service}Service, {Service}Client, and Register{Service} bindings with synchronous methods and explicit Result values.
  • C++ bindings that use protobuf's generated C++ full or lite messages and adapt them to the byte-oriented trevrpc-c transport without protobuf-c.
  • Kotlin .trevrpc.kt bindings that use protobuf Java messages, suspend functions, and Flow.
  • JavaScript .trevrpc.js client files with companion .trevrpc.d.ts TypeScript declarations.

See Protobuf and Code Generation for generator options and language-specific output shapes.

3. Add the Runtime

For Rust, add the trevrpc runtime to your application crate using the package source your project uses for Trev packages. The default crate features include client, server, Quinn, HTTP/3 server, and WebTransport support.

For Go, add the runtime module:

go get trev.zip/llc/trevrpc/trevrpc-go

The Go runtime uses github.com/quic-go/quic-go for native QUIC and HTTP/3, plus github.com/quic-go/webtransport-go for WebTransport. The Rust runtime uses Quinn, h3, h3-quinn, and h3-webtransport for unified server handling; its native WebTransport client uses web-transport-quinn.

The C++20 runtime and generator build from source under trevrpc-cpp/. Generate normal protobuf C++ messages and TrevRPC C++ service bindings from the same schema, compile both generated outputs, and link them with trevrpc::cpp and the selected protobuf C++ runtime. Both full and lite protobuf are supported; C++ applications do not generate or link protobuf-c output. See C++ Guide.

The Kotlin modules currently build from source rather than from published Maven artifacts. Include trevrpc-kotlin in your Gradle build and depend on :core plus :transport-netty for JVM native QUIC and HTTP/3, or :transport-cronet for Android-oriented HTTP/3. See Kotlin Guide.

For JavaScript or TypeScript browser clients, add the runtime package:

npm install trevrpc-js

The JavaScript runtime uses the browser WebTransport API and is currently client-side only.

4. Implement the Server

Rust generated services are async traits. A unary method implementation looks like this:

struct GreeterService;

#[trevrpc::async_trait]
impl greeter::Greeter for GreeterService {
    async fn say_hello(
        &self,
        request: greeter::HelloRequest,
    ) -> Result<greeter::HelloReply, trevrpc::Status> {
        Ok(greeter::HelloReply {
            message: format!("hello, {}", request.name),
        })
    }
}

Go generated services are interfaces:

type greeterService struct{}

func (greeterService) SayHello(_ context.Context, request *greeter.HelloRequest) (*greeter.HelloReply, error) {
	return &greeter.HelloReply{Message: "hello " + request.Name}, nil
}

Kotlin generated services use suspend functions and Flow:

class GreeterServiceImpl : GreeterService {
    override suspend fun sayHello(
        context: RequestContext,
        request: HelloRequest,
    ): HelloReply =
        HelloReply.newBuilder()
            .setMessage("hello ${request.name}")
            .build()
}

C++ generated services are synchronous {Service}Service interfaces. Unary and client-streaming handlers return Result<Response>; server-streaming and bidirectional handlers return Status after writing responses through typed stream adapters.

Return trevrpc::Status in Rust or *trevrpc.Status in Go when you need a specific RPC status code. Kotlin handlers throw TrevRpcException(Status...). C++ handlers return errors through Result.

5. Register Routes

Generated registration functions install handlers on a TrevRPC server.

Rust:

let mut server = trevrpc::server::Server::new();
greeter::register_greeter(&mut server, GreeterService);

Go:

server := trevrpc.NewServer()
greeter.RegisterGreeterServer(server, greeterService{})

Kotlin:

val server = Server()
registerGreeter(server, GreeterServiceImpl())

C++ registration uses Register{Service}(Server&, std::shared_ptr<{Service}Service>) and returns Result<void>. The server's route adapters retain the shared service implementation until the server closes.

Set authorization, metrics, and server limits before serving traffic.

6. Configure a Transport

Native TrevRPC over QUIC expects TLS ALPN trevrpc/1.

Rust:

server_crypto.alpn_protocols = vec![trevrpc::ALPN.to_vec()];
client_crypto.alpn_protocols = vec![trevrpc::ALPN.to_vec()];

Go:

tlsConfig := &tls.Config{
	NextProtos: []string{trevrpc.ALPN},
}

Applications own certificate selection, trust roots, server-name verification, and mTLS policy.

Kotlin Netty endpoints use NettyServerTls and NettyClientTls. NettyRpcServerConfig advertises trevrpc/1, h3, or both according to the enabled transports. Keep hostname verification enabled and use explicit trust roots in production.

WebTransport uses HTTP/3 WebTransport configuration instead of the native TrevRPC ALPN. See WebTransport for Rust and Go setup.

Ordinary HTTP/3 uses ALPN h3, an HTTP POST, and the same TrevRPC frames in the request and response bodies. See HTTP/3 for server setup.

7. Serve Traffic

Rust serves a caller-created Quinn endpoint:

server.serve_quinn(endpoint).await?;

Use serve_quinn_with_shutdown when you have a shutdown future and want graceful draining.

Go serves a quic-go listener:

if err := trevrpc.ServeQUIC(ctx, listener, server); err != nil {
	log.Fatal(err)
}

Cancel the serving context to begin shutdown.

Kotlin binds a single Netty UDP listener:

val listener = NettyRpcServer.bind(
    server,
    NettyRpcServerConfig(
        bindAddress = InetSocketAddress("127.0.0.1", 50051),
        tls = NettyServerTls.Pem(File("server-key.pem"), File("server-cert.pem")),
        enableNative = true,
        enableHttp3 = true,
    ),
)

Call listener.shutdown() to stop admission, drain active work, and release transport resources.

The C++ Server is a synchronous facade over the trevrpc-c server transport. Create it with Server::listen, register services before calling the blocking serve(), and call shutdown() before destroying server state.

8. Create a Client

Generated clients use a long-lived channel that reconnects for future work. Every RPC snapshots one ready connection generation and never moves from it. If that generation dies, the RPC fails without retry, replay, or resumption. New calls fail immediately with Unavailable or its runtime equivalent while the channel reconnects; call the runtime readiness method first when waiting is intentional.

Rust:

let channel = trevrpc::client::Channel::connect(
    endpoint.clone(),
    addr,
    "localhost",
).await?;
let client = greeter::GreeterClient::new(channel.clone());

let reply = client
    .say_hello(
        greeter::HelloRequest { name: "TrevRPC".into() },
        trevrpc::client::CallOptions::new()
            .with_timeout(std::time::Duration::from_secs(5)),
    )
    .await?;

Go:

channel, err := trevrpc.Dial(ctx, addr, trevrpc.DialOptions{TLSConfig: tlsConfig})
if err != nil {
	log.Fatal(err)
}
defer channel.Close()

client := greeter.NewGreeterClient(
	channel,
	trevrpc.WithTimeout(5*time.Second),
)

reply, err := client.SayHello(ctx, &greeter.HelloRequest{Name: "TrevRPC"})

C native MsQuic:

trevrpc_config config = trevrpc_default_config();
trevrpc_channel* channel = NULL;
int rc = trevrpc_channel_connect(
    "127.0.0.1", 50051, &config, NULL, 5000000000ull, NULL, &channel);
if (rc != 0) {
    return rc;
}

Hello__V1__HelloRequest request = HELLO__V1__HELLO_REQUEST__INIT;
request.name = "TrevRPC";
Hello__V1__HelloReply* reply = NULL;
rc = hello_v1_greeter_say_hello(channel, &request, &reply);
if (reply != NULL) {
    hello__v1__hello_reply__free_unpacked(reply, NULL);
}

trevrpc_channel_close(channel);
trevrpc_channel_release(channel);

Generated C methods accept trevrpc_channel* directly. Call trevrpc_channel_wait_ready before new work when the application intentionally waits through reconnection. Routine C channel coverage is native MsQuic QUIC; single-connection native QUIC and WebTransport APIs are advanced interfaces in trevrpc_raw.h. Always close, then release, the channel.

C++20 clients use the same underlying byte transport through a long-lived Channel, but accept and return protobuf C++ messages:

auto connected = trevrpc::Channel::connect("127.0.0.1", 50051, config, 5s);
auto channel = std::move(connected).value();
hello::v1::GreeterClient client(channel);

hello::v1::HelloRequest request;
request.set_name("TrevRPC");
auto reply = client.SayHello(request);
if (!reply) {
  std::cerr << reply.error().message() << '\n';
}

Share the Channel across generated {Service}Client instances. A call remains pinned to the ready connection generation on which it starts and fails if that generation is lost; it is never retried, replayed, resumed, or moved. Reconnection only enables future calls. Wait for readiness only when the application intentionally wants to wait through reconnection, then close the channel during shutdown. See C++ Guide for all four synchronous RPC shapes.

Kotlin Netty native QUIC:

val config = NettyQuicClientConfig(
    remoteAddress = InetSocketAddress("127.0.0.1", 50051),
    tls = NettyClientTls("localhost", trustCertificates = listOf(certificate)),
)
val channel: RpcChannel = NettyRpcChannel.nativeQuic(config)
channel.awaitReady()
val client = GreeterClient(channel, CallOptions(timeout = 5.seconds))
val reply = client.sayHello(
    HelloRequest.newBuilder().setName("TrevRPC").build(),
)

Use NettyRpcChannel.http3(config) for JVM HTTP/3. Android applications use CronetRpcChannel.create(engine, origin, callbackExecutor) with an application-owned CronetEngine and executor. Kotlin does not currently provide a WebTransport client.

At shutdown, call channel.close().

JavaScript and TypeScript WebTransport clients use generated .trevrpc.js modules:

import { connect } from "trevrpc-js";
import { GreeterClient } from "./hello/v1/greeter.trevrpc.js";

const channel = await connect(
  "https://127.0.0.1:50051/trevrpc",
);
const client = new GreeterClient(channel, {
  timeoutMs: 5000,
  metadata: { authorization: "Bearer trevrpc-example-token" },
});

try {
  const reply = await client.sayHello({ name: "TrevRPC" });
} finally {
  channel.close({ closeCode: 0, reason: "client shutdown" });
}

The browser root connect API returns a Channel over WebTransport. Node uses the same root API through package exports, or Channel from trevrpc-js/node, backed by native MsQuic. Use waitUntilReady() before issuing new calls when the application intentionally waits through a reconnect.

Raw single-connection transports are advanced-only and intended for integrations, benchmarks, diagnostics, and deterministic tests. See Connection Lifecycle for the complete routine and advanced API list, readiness, shutdown, migration, and TLS-resumption behavior.

9. Set Production Defaults

For real applications, configure these before exposing a server:

  • TLS certificates, trust roots, and the selected transport negotiation.
  • Client call deadlines.
  • Server concurrency and stream limits.
  • Request metadata authorization or transport-level identity.
  • Metrics hooks.
  • Graceful shutdown behavior.
  • Channel readiness and reconnect behavior.

See Operations, Limits, and Security for the available knobs and defaults.

Next Steps