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.
Prerequisites
Section titled “Prerequisites”- 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.
Create a project
Section titled “Create a project”First, we’ll need to create a new Rust project and add some dependencies:
$ 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 fullconnectrpc 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.
Define a service
Section titled “Define a service”Now we’re ready to write the Protocol Buffer schema that defines our service. In your shell,
$ mkdir -p greet/v1$ touch greet/v1/greet.protoOpen 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.
Generate code
Section titled “Generate code”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: v2lint: use: - STANDARDbreaking: use: - FILEWe 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: v2plugins: - 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::protoWith those configuration files in place, you can lint your schema and generate code:
$ buf lint$ buf generateIn your src/gen directory, you should now see some generated Rust:
src/gen├── buffa│ └── greet.v1.rs└── connect └── greet.v1.rsbuffa/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.
Generating code at build time
Section titled “Generating code at build time”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:
$ cargo add --build connectrpc-buildThen, 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.
Implement handler
Section titled “Implement handler”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:
$ cargo run --bin serverMake requests
Section titled “Make requests”The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload:
$ curl \ --header "Content-Type: application/json" \ --data '{"name": "Jane"}' \ http://localhost:8080/greet.v1.GreetService/GreetThis responds:
{"greeting":"Hello, Jane!"}Your new handler automatically supports gRPC requests, too:
$ buf curl \ --schema ./greet/v1/greet.proto \ --protocol grpc \ --http2-prior-knowledge \ --data '{"name": "Jane"}' \ http://localhost:8080/greet.v1.GreetService/GreetThis responds:
{ "greeting": "Hello, Jane!"}Or, with the Connect protocol:
$ buf curl \ --schema . \ --data '{"name": "Jane"}' \ http://localhost:8080/greet.v1.GreetService/GreetWe 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:
$ cargo run --bin clientCongratulations — 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-rustcompatible with other gRPC implementations out of the box.tonicclients will work withconnect-rustservers and vice versa without issue. - The gRPC-Web protocol used by grpc/grpc-web, allowing
connect-rustservers to interop withgrpc-webfrontends 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:
$ cargo run --bin clientYour output should remain the same, but connect-rust is now using the gRPC
protocol instead of the Connect protocol to communicate over the wire.
So what?
Section titled “So what?”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.