Skip to content
Created by

Getting started

Connect is a slim library for building browser- and gRPC-compatible HTTP APIs. You define your service with a Protocol Buffer schema, and Connect generates type-safe server and client code. Fill in your server’s business logic and you’re done — no hand-written marshaling, routing, or client code required!

This fifteen-minute walkthrough helps you create a small Connect service in Rust. It demonstrates what you’ll be writing by hand, what Connect generates for you, and how to call your new API.

  • You’ll need Rust 1.88 or newer. See Rust’s installation guide for instructions.
  • The Buf CLI installed, and include it in the $PATH.
  • We’ll also use cURL. It’s available from Homebrew and most Linux package managers.

First, we’ll need to create a new Rust project and add some dependencies:

Terminal window
$ cargo new --lib connect-rust-example
$ cd connect-rust-example
$ cargo add connectrpc --features server,client
$ cargo add buffa --features json
$ cargo add buffa-types --features json
$ cargo add serde --features derive
$ cargo add tokio --features full

connectrpc is the Connect runtime. Its server and client features enable the built-in HTTP server and client. Connect for Rust uses buffa, a pure-Rust Protobuf implementation, for messages, and buffa-types for Protobuf’s well-known types like Timestamp. Their json features, together with serde, are what let your API speak JSON as well as binary Protobuf.

Now we’re ready to write the Protocol Buffer schema that defines our service. In your shell,

Terminal window
$ mkdir -p greet/v1
$ touch greet/v1/greet.proto

Open greet/v1/greet.proto in your editor and add:

syntax = "proto3";
package greet.v1;
message GreetRequest {
string name = 1;
}
message GreetResponse {
string greeting = 1;
}
service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse) {}
}

This file declares the greet.v1 Protobuf package, a service called GreetService, and a single method called Greet with its request and response structures. These package, service, and method names will reappear soon in our HTTP API’s URLs.

We’re going to generate our code using Buf, a modern replacement for Google’s protobuf compiler. (If you’d prefer, you can generate code from build.rs instead — see below.)

First, scaffold a basic buf.yaml by running buf config init. It should contain:

version: v2
lint:
use:
- STANDARD
breaking:
use:
- FILE

We will use remote plugins, a feature of the Buf Schema Registry for generating code. Tell Buf how to generate code by putting this into buf.gen.yaml:

version: v2
plugins:
- remote: buf.build/anthropics/buffa
out: src/gen/buffa
opt:
- file_per_package=true
- json=true
- remote: buf.build/connectrpc/rust
out: src/gen/connect
opt:
- file_per_package=true
- buffa_module=crate::proto

With those configuration files in place, you can lint your schema and generate code:

Terminal window
$ buf lint
$ buf generate

In your src/gen directory, you should now see some generated Rust:

src/gen
├── buffa
│ └── greet.v1.rs
└── connect
└── greet.v1.rs

buffa/greet.v1.rs was generated by the anthropics/buffa plugin, and it contains the GreetRequest and GreetResponse structs and the associated marshaling code. connect/greet.v1.rs was generated by connectrpc/rust, and it contains the GreetService trait for servers and the GreetServiceClient struct for clients. Feel free to poke around if you’re interested — connect/greet.v1.rs is standard Rust code.

Each plugin writes one file per Protobuf package, so we need to tell Rust where to find them. Replace the contents of src/lib.rs with a module for the greet.v1 package that includes both files:

pub mod proto {
pub mod greet {
pub mod v1 {
include!("gen/buffa/greet.v1.rs");
include!("gen/connect/greet.v1.rs");
}
}
}

The buffa_module=crate::proto option we passed to the Connect plugin tells it that the message types live under crate::proto.

Rust projects often generate code from a build.rs script instead of checking it in. Connect for Rust supports this with the connectrpc-build crate, which can run Buf for you. Add it as a build dependency:

Terminal window
$ cargo add --build connectrpc-build

Then, instead of buf.gen.yaml, create build.rs:

fn main() {
connectrpc_build::Config::new()
.files(&["greet/v1/greet.proto"])
.use_buf()
.include_file("_connectrpc.rs")
.compile()
.unwrap();
}

And mount the generated code in src/lib.rs with include_generated!:

pub mod proto {
connectrpc::include_generated!();
}

Both approaches produce the same proto::greet::v1 module, so the rest of this guide works with either one.

The code we’ve generated takes care of the boring boilerplate, but we still need to implement our greeting logic. In the generated code, this is represented as the GreetService trait. Since the trait is so small, we can do everything in one file. mkdir -p src/bin, then add src/bin/server.rs:

use std::sync::Arc;
use connect_rust_example::proto::greet::v1::{GreetRequest, GreetResponse, GreetService};
use connectrpc::{RequestContext, Response, Router, Server, ServiceRequest, ServiceResult};
struct GreetServer;
// Handlers may return any type that encodes as a GreetResponse.
#[allow(refining_impl_trait)]
impl GreetService for GreetServer {
async fn greet(
&self,
_ctx: RequestContext,
request: ServiceRequest<'_, GreetRequest>,
) -> ServiceResult<GreetResponse> {
Response::ok(GreetResponse {
greeting: format!("Hello, {}!", request.name),
..Default::default()
})
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let router = Router::new().add_service(Arc::new(GreetServer));
Server::new(router).serve("127.0.0.1:8080".parse()?).await?;
Ok(())
}

In a separate terminal window, you can now start your server:

Terminal window
$ cargo run --bin server

The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload:

Terminal window
$ curl \
--header "Content-Type: application/json" \
--data '{"name": "Jane"}' \
http://localhost:8080/greet.v1.GreetService/Greet

This responds:

{"greeting":"Hello, Jane!"}

Your new handler automatically supports gRPC requests, too:

Terminal window
$ buf curl \
--schema ./greet/v1/greet.proto \
--protocol grpc \
--http2-prior-knowledge \
--data '{"name": "Jane"}' \
http://localhost:8080/greet.v1.GreetService/Greet

This responds:

{
"greeting": "Hello, Jane!"
}

Or, with the Connect protocol:

Terminal window
$ buf curl \
--schema . \
--data '{"name": "Jane"}' \
http://localhost:8080/greet.v1.GreetService/Greet

We can also make requests using Connect’s generated client. Put this in src/bin/client.rs:

use connect_rust_example::proto::greet::v1::{GreetRequest, GreetServiceClient};
use connectrpc::client::{ClientConfig, HttpClient};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = GreetServiceClient::new(
HttpClient::plaintext(),
ClientConfig::new("http://localhost:8080".parse()?),
);
let res = client
.greet(GreetRequest {
name: "Jane".into(),
..Default::default()
})
.await?;
println!("{}", res.view().greeting);
Ok(())
}

With your server still running in a separate terminal window, you can now run your client:

Terminal window
$ cargo run --bin client

Congratulations — you’ve built your first Connect service! 🎉

Use the gRPC protocol instead of the Connect protocol

Section titled “Use the gRPC protocol instead of the Connect protocol”

connect-rust supports three protocols:

  • The new Connect protocol, a simple, HTTP-based protocol that works over HTTP/1.1 or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including streaming, and packages them into a protocol that works equally well in browsers, monoliths, and microservices. The Connect protocol is what we think the gRPC protocol should be. By default, JSON- and binary-encoded Protobuf is supported.
  • The gRPC protocol that is used throughout the gRPC ecosystem, making connect-rust compatible with other gRPC implementations out of the box. tonic clients will work with connect-rust servers and vice versa without issue.
  • The gRPC-Web protocol used by grpc/grpc-web, allowing connect-rust servers to interop with grpc-web frontends without the need for an intermediary proxy (such as Envoy).

By default, connect-rust servers support ingress from all three protocols without any configuration. connect-rust clients use the Connect protocol by default, but can use either the gRPC or gRPC-Web protocols by calling with_protocol on the client’s ClientConfig.

Edit src/bin/client.rs above to create the GreetServiceClient using the gRPC protocol:

let client = GreetServiceClient::new(
HttpClient::plaintext_http2_only(),
ClientConfig::new("http://localhost:8080".parse()?)
.with_protocol(connectrpc::Protocol::Grpc),
);

The gRPC protocol requires HTTP/2, so we also switched to an HTTP client that uses HTTP/2 without TLS. With your server still running in a separate terminal window, run the client one more time:

Terminal window
$ cargo run --bin client

Your output should remain the same, but connect-rust is now using the gRPC protocol instead of the Connect protocol to communicate over the wire.

With just a few lines of hand-written code, you’ve built a real API server that supports both the gRPC and Connect protocols. Unlike a hand-written REST service, you didn’t need to design a URL hierarchy, hand-write request and response structs, manage your own marshaling, or parse typed values out of query parameters. More importantly, your users got an idiomatic, type-safe client without any extra work on your part.