Getting started
Connect is a slim library for building HTTP APIs consumable anywhere, including browsers. 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 Python. 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”- uv installed. Any package manager, including pip, can also be used.
- 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.
Set up Python environment
Section titled “Set up Python environment”First, we’ll set up the Python environment and dependencies.
uv inituv add connectrpc uvicornuv inituv add connectrpc gunicornDefine 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 proto/greet/v1$ touch proto/greet/v1/greet.protoOpen proto/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.
First, scaffold a basic buf.yaml by running buf config init.
Then, edit buf.yaml to use our proto directory:
version: v2modules: - path: protolint: use: - DEFAULTbreaking: use: - FILEWe will use remote plugins, a feature of the
Buf Schema Registry for generating code.
Tell Buf how to generate code by creating a buf.gen.yaml:
$ touch buf.gen.yamlversion: v2plugins: - remote: buf.build/protocolbuffers/python out: . - remote: buf.build/protocolbuffers/pyi out: . - remote: buf.build/connectrpc/python out: .With those configuration files in place, you can lint your schema and generate code:
$ buf lint$ buf generateIn the greet package, you should now see some generated Python:
greet └── v1 ├── greet_connect.py └── greet_pb2.py └── greet_pb2.pyiThe package greet/v1 contains greet_pb2.py and greet_pb2.pyi which were generated by
the protocolbuffers/python and
protocolbuffers/pyi and contain GreetRequest
and GreetResponse structs and the associated marshaling code. greet_connect.py was
generated by connectrpc/python and contains the
WSGI and ASGI service interfaces and client code to access a Connect server. Feel free to
poke around if you’re interested - greet_connect.py is standard Python code.
Implement service
Section titled “Implement service”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 greet_connect.GreetService and greet_connect.GreetServiceSync
interfaces for async ASGI and sync WSGI servers. Since the interface is so small, we can do everything
in one Python file. touch server.py and add:
from greet.v1.greet_connect import GreetService, GreetServiceASGIApplicationfrom greet.v1.greet_pb2 import GreetResponse
class Greeter(GreetService): async def greet(self, request, ctx): print("Request headers: ", ctx.request_headers()) response = GreetResponse(greeting=f"Hello, {request.name}!") ctx.response_headers()["greet-version"] = "v1" return response
app = GreetServiceASGIApplication(Greeter())from greet.v1.greet_connect import GreetServiceSync, GreetServiceWSGIApplicationfrom greet.v1.greet_pb2 import GreetResponse
class Greeter(GreetServiceSync): def greet(self, request, ctx): print("Request headers: ", ctx.request_headers()) response = GreetResponse(greeting=f"Hello, {request.name}!") ctx.response_headers()["greet-version"] = "v1" return response
app = GreetServiceWSGIApplication(Greeter())In a separate terminal window, you can now start your server:
uv run uvicorn server:appuv run gunicorn server:appMake requests
Section titled “Make requests”The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload. If you have a recent version of cURL installed, it’s a one-liner:
$ curl \ --header "Content-Type: application/json" \ --data '{"name": "Jane"}' \ http://localhost:8000/greet.v1.GreetService/GreetThis responds:
{ "greeting": "Hello, Jane!"}We can also make requests using Connect’s generated client. touch client.py and add:
import asyncio
from greet.v1.greet_connect import GreetServiceClientfrom greet.v1.greet_pb2 import GreetRequest
async def main(): client = GreetServiceClient("http://localhost:8000") res = await client.greet(GreetRequest(name="Jane")) print(res.greeting)
if __name__ == "__main__": asyncio.run(main())from greet.v1.greet_connect import GreetServiceClientSyncfrom greet.v1.greet_pb2 import GreetRequest
def main(): client = GreetServiceClientSync("http://localhost:8000") res = client.greet(GreetRequest(name="Jane")) print(res.greeting)
if __name__ == "__main__": main()With your server still running in a separate terminal window, you can now run your client:
$ uv run python client.pyCongratulations — you’ve built your first Connect service! 🎉
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.