Skip to main content

Walkthrough: gRPC Greeter

Sample: examples/grpc_greeter · Needs: nothing but Dart (a gRPC client to call it)

A minimal gRPC service — one unary method, SayHello — running alongside the normal Vania HTTP server. gRPC is a high-performance RPC protocol built on Protocol Buffers; this sample shows how to stand a service up with vania_grpc and, as a bonus, demystifies what protobuf encoding actually is.

See the gRPC package page for the full API.

The service definition

In gRPC terms:

service greeter.Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest { string name = 1; }
message HelloResponse { string message = 1; }

Run it

cd examples/grpc_greeter
dart pub get
dart run bin/server.dart

The gRPC server listens on GRPC_PORT (default 50051); the HTTP server also comes up on APP_PORT. Call it with any gRPC client, for example grpcurl:

grpcurl -plaintext -d '{"name":"Vania"}' \
localhost:50051 greeter.Greeter/SayHello
# { "message": "Hello, Vania!" }

(Reflection isn't enabled, so the method name is passed explicitly.)

Registering the service

// lib/config/app.dart
'providers': <ServiceProvider>[
GrpcServiceProvider(
services: [GreeterService()],
config: GrpcConfig(
host: env('GRPC_HOST', '0.0.0.0'),
port: env<int>('GRPC_PORT', 50051),
autoStart: true, // start the gRPC server when the app boots
),
),
],

services: is the list of gRPC services to expose; autoStart: true brings the server up automatically at boot.

The service: rule as a pure function, plus a handler

The greeting rule is a plain function, kept separate so it can be tested without a gRPC call:

// lib/greeter/greeter_service.dart
String greetingFor(String name) =>
'Hello, ${name.trim().isEmpty ? 'World' : name.trim()}!';

The service registers the SayHello method and delegates to that function:

class GreeterService extends Service {
GreeterService() {
$addMethod(ServiceMethod<HelloRequest, HelloResponse>(
'SayHello',
sayHello,
false, // client streaming?
false, // server streaming?
HelloRequest.fromBuffer, // how to decode the request
(response) => response.writeToBuffer(),// how to encode the response
));
}

@override
String get $name => 'greeter.Greeter';

Future<HelloResponse> sayHello(ServiceCall call, Future<HelloRequest> request) async {
final req = await request;
return HelloResponse(message: greetingFor(req.name));
}
}

The two false flags say this is a unary call — one request, one response, no streaming. The two function arguments tell gRPC how to turn bytes into a HelloRequest and a HelloResponse back into bytes. In a real project those (de)serializers come from generated code.

What protobuf actually is (the honest bit)

Normally you write a .proto file and run protoc to generate the message classes. To keep the sample dependency-free, it hand-writes the wire encoding for a message with a single string field:

// lib/greeter/hello_messages.dart
List<int> _encodeStringField1(String value) {
final bytes = utf8.encode(value);
return [0x0A, ..._writeVarint(bytes.length), ...bytes]; // tag 1, wire type 2
}

That 0x0A is not magic: it is field number 1, wire type 2 (length-delimited), packed into one byte (1 << 3 | 2). Then comes the length as a varint, then the UTF-8 bytes. The decoder reverses it. You will almost never write this by hand — but seeing it once makes protobuf feel a lot less like a black box: it is just tagged, length-prefixed fields.

class HelloRequest {
const HelloRequest({this.name = ''});
final String name;

List<int> writeToBuffer() => _encodeStringField1(name);
static HelloRequest fromBuffer(List<int> bytes) =>
HelloRequest(name: _decodeStringField1(bytes));
}

In a real service, replace hello_messages.dart with protoc-generated classes and nothing else about the service changes.

Testing without a server

Everything is testable in-process: the message codec round-trips (encode then decode returns the original), greetingFor is a pure function, and sayHello can be invoked directly with a Future<HelloRequest>:

dart test

What to take away

  • A gRPC service is registered through GrpcServiceProvider; autoStart runs it on boot, next to your HTTP server.
  • Keep the actual rule (greetingFor) a pure function; the service method is a thin wrapper.
  • Protobuf messages are tagged, length-prefixed fields — normally generated by protoc, hand-written here only to avoid codegen.
  • Unary vs streaming is set by the two boolean flags on the method.