Dart 77 🎯 Microservices and gRPC Services Implementation in Dart (package:grpc)
A microservices architecture splits an application into small, independently deployable services that communicate over a network. The communication protocol matters: HTTP/JSON is flexible but lacks a strict contract, while gRPC provides a strongly typed interface defined in Protocol Buffers, generated client and server stubs, and native support for streaming in both directions. Dart’s package:grpc implements gRPC for both the server and the client, using the same .proto contract that any other gRPC language uses . This means a Dart microservice can call a Go service, a Python service, or a Java service without writing a custom client for each one, because the contract is the .proto file and the generated code is the language-specific binding to that contract.
The workflow has four stages: define the service in a .proto file, generate the Dart code with protoc and the Dart plugin, implement the server by extending the generated base class, and call the service from the client using the generated stub. The package:grpc library provides the runtime for both sides, including the Server, the ClientChannel, and the ServiceCall context that carries metadata, deadlines, and cancellation . The .proto file is the source of truth; the generated code is derived from it, and it must be regenerated whenever the contract changes.
Key point: A gRPC service in Dart is defined in a .proto file and compiled with protoc --dart_out=grpc:... to generate message classes and service stubs. The server extends the generated *ServiceBase class and implements the RPC methods; the client creates a ClientChannel and a generated *Client stub. The .proto file is the contract, and the generated code is the binding, so the contract must be regenerated after every change.
Why gRPC fits microservices in Dart
The contract problem. A REST API’s contract is documentation, not code. The client sends a JSON body and hopes the server accepts it; the server parses the JSON and hopes the client sent the right fields. A gRPC contract is a .proto file that both sides compile. The generated code enforces the types at compile time, and the serialization is handled by the Protocol Buffers runtime . A field rename in the .proto file becomes a compile error in both the client and the server, not a runtime surprise.
The streaming problem. REST over HTTP/1.1 is request-response: the client sends one request and receives one response. gRPC supports four communication patterns: unary (one request, one response), server streaming (one request, a stream of responses), client streaming (a stream of requests, one response), and bidirectional streaming (both sides send streams) . The streaming is built into the protocol and the generated stubs, so the application does not need to implement a separate WebSocket layer or a polling mechanism.
The performance problem. gRPC uses HTTP/2 as its transport, which multiplexes multiple calls over a single connection and uses binary framing. Protocol Buffers are more compact than JSON, and the serialization is faster because it is schema-driven rather than reflective . For internal microservice communication, where the contract is stable and the payloads are structured, gRPC is more efficient than JSON over HTTP/1.1.
The multi-language problem. A microservices architecture is rarely homogeneous. One team writes Go, another writes Python, another writes Dart. The .proto file is language-neutral; the protoc compiler generates the bindings for each language. The Dart service and the Go service share the same contract and the same wire format, so the choice of language is an implementation detail rather than a barrier to integration .
a. Defining the service and generating the code
The .proto file defines the messages and the service. The syntax declaration selects the Protocol Buffers version, the message blocks define the data structures, and the service block defines the RPC methods. The stream keyword on the request or response type selects the streaming pattern.
// Use Protocol Buffers version 3
syntax = "proto3";
// The package prevents name collisions between services
package greeter;
// The service definition
// Each rpc line defines a method and its request/response types
service Greeter {
// Unary RPC: one request, one response
rpc SayHello (HelloRequest) returns (HelloReply);
// Server streaming RPC: one request, a stream of responses
rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}
// The request message
message HelloRequest {
// The field number is the wire identifier, not the position
// It must be unique and stable for backward compatibility
string name = 1;
}
// The response message
message HelloReply {
string message = 1;
}
The field numbers are the wire identifiers. They are not the field positions and they must not be reused. Changing a field number is a breaking change; adding a new field with a new number is backward-compatible .
The generation command uses protoc with the Dart plugin. The --dart_out=grpc:lib/src/generated flag tells the plugin to generate the gRPC stubs in addition to the message classes, and to write the output to lib/src/generated .
# Activate the Dart protoc plugin globally
# This installs the plugin that protoc uses to generate Dart code
dart pub global activate protoc_plugin
# Add the plugin's directory to PATH so protoc can find it
export PATH="$PATH:$HOME/.pub-cache/bin"
# Generate the Dart code from the proto file
# --dart_out=grpc:lib/src/generated means: generate gRPC code into that directory
# -Iprotos tells protoc where to find the imports
# protos/greeter.proto is the contract file
protoc --dart_out=grpc:lib/src/generated -Iprotos protos/greeter.proto
The generated files are greeter.pb.dart (the message classes), greeter.pbgrpc.dart (the client stub and the server base class), and greeter.pbenum.dart if the proto defines enums . The server imports the .pbgrpc.dart file and extends the GreeterServiceBase class. The client imports the same file and creates a GreeterClient with a ClientChannel.
b. The server: extending the base class
The generated *ServiceBase class has an abstract method for every RPC in the .proto file. The server class extends it and overrides the methods. The method receives a ServiceCall (the context) and the request message, and returns the response message or a Stream of responses.
// Import the gRPC runtime for the Server class
import 'package:grpc/grpc.dart';
// Import the generated code for the service base and the messages
import 'generated/greeter.pbgrpc.dart';
// The service implementation extends the generated base class
class GreeterService extends GreeterServiceBase {
// The unary method: one request, one response
@override
Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) async {
// The response is a generated message class
// The cascade operator sets the message field
return HelloReply()..message = 'Hello, ${request.name}!';
}
// The server streaming method: one request, a stream of responses
@override
Stream<HelloReply> sayHelloStream(ServiceCall call, HelloRequest request) async* {
// Yield three responses to demonstrate streaming
for (var i = 0; i < 3; i++) {
// Each yield sends one message to the client
yield HelloReply()..message = 'Hello ${request.name}, part $i';
}
}
}
Future<void> main() async {
// Create the server with the service implementation
final server = Server([GreeterService()]);
// Start listening on port 50051
// The default is to listen on all interfaces
await server.serve(port: 50051);
print('Server listening on port ${server.port}');
}
The ServiceCall object is the context of the RPC. It carries the client’s metadata (headers), the deadline, and the cancellation signal. The server can check call.cancelled to stop long-running work when the client disconnects, and it can read call.clientMetadata for authentication tokens or tracing IDs .
The Server class takes a list of service implementations and a set of options. The CodecRegistry is the serializer registry; the default is Protocol Buffers, which is what the generated code uses. The Server also supports interceptors, which are functions that wrap the RPC handling and can be used for authentication, logging, or metrics.
c. The client: the channel and the stub
The client creates a ClientChannel that points to the server’s address and port. The channel manages the HTTP/2 connection, the load balancing (if there are multiple addresses), and the connection lifecycle. The generated *Client stub takes the channel and provides a method for each RPC.
// Import the gRPC runtime for ClientChannel and ChannelCredentials
import 'package:grpc/grpc.dart';
// Import the generated client stub and the messages
import 'generated/greeter.pbgrpc.dart';
Future<void> main() async {
// Create a channel to the server
// The credentials are insecure for local development
// In production, use ChannelCredentials.secure with certificates
final channel = ClientChannel(
'localhost',
port: 50051,
options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
);
// Create the client stub with the channel
final client = GreeterClient(channel);
try {
// Call the unary RPC
// The request is a generated message
final response = await client.sayHello(HelloRequest()..name = 'Dart');
print('Unary response: ${response.message}');
// Call the server streaming RPC
// The returned stream yields each response as it arrives
final stream = client.sayHelloStream(HelloRequest()..name = 'Dart');
await for (final reply in stream) {
print('Stream response: ${reply.message}');
}
} finally {
// Shut down the channel to release the connection
await channel.shutdown();
}
}
The ChannelCredentials.insecure() is only appropriate for local development or a trusted network. For production, the channel uses ChannelCredentials.secure() with a certificate authority, or the platform’s default trust store . The channel.shutdown() call is the cleanup; without it, the process may hang because the HTTP/2 connection is still open.
The client stub also supports metadata. The options parameter on the RPC call accepts a CallOptions object with metadata (a map of headers), timeout (a deadline), and cancellation (a CancellationToken) . The metadata is how the client sends authentication tokens, and the timeout is how it prevents a slow server from blocking the caller indefinitely.
Complete Example Session
The following session builds a complete gRPC service with a unary RPC and a server streaming RPC, generates the code, implements the server, and calls it from the client.
// ============================================
// PART 1: THE PROTO CONTRACT
// ============================================
syntax = "proto3";
package greeter;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
# ============================================
# PART 2: GENERATE THE CODE
# ============================================
dart pub global activate protoc_plugin
export PATH="$PATH:$HOME/.pub-cache/bin"
protoc --dart_out=grpc:lib/src/generated -Iprotos protos/greeter.proto
# Generated: greeter.pb.dart, greeter.pbgrpc.dart
// ============================================
// PART 3: THE SERVER
// ============================================
import 'package:grpc/grpc.dart';
import 'src/generated/greeter.pbgrpc.dart';
class GreeterService extends GreeterServiceBase {
@override
Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) async {
return HelloReply()..message = 'Hello, ${request.name}!';
}
@override
Stream<HelloReply> sayHelloStream(ServiceCall call, HelloRequest request) async* {
for (var i = 0; i < 3; i++) {
yield HelloReply()..message = 'Hello ${request.name}, part $i';
}
}
}
Future<void> main() async {
final server = Server([GreeterService()]);
await server.serve(port: 50051);
print('Server on port ${server.port}');
}
// ============================================
// PART 4: THE CLIENT
// ============================================
import 'package:grpc/grpc.dart';
import 'src/generated/greeter.pbgrpc.dart';
Future<void> main() async {
final channel = ClientChannel(
'localhost',
port: 50051,
options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
);
final client = GreeterClient(channel);
try {
final response = await client.sayHello(HelloRequest()..name = 'Dart');
print('Unary: ${response.message}');
final stream = client.sayHelloStream(HelloRequest()..name = 'Dart');
await for (final reply in stream) {
print('Stream: ${reply.message}');
}
} finally {
await channel.shutdown();
}
}
The four parts cover the proto contract, the code generation, the server implementation, and the client call. What the session demonstrates is that the .proto file is the single source of truth, the generated code provides the base class and the stub, and the runtime handles the serialization and the HTTP/2 transport.
Quick Reference
Workflow Commands
| Step | Command |
|---|---|
| Activate plugin | dart pub global activate protoc_plugin |
| Update PATH | export PATH="$PATH:$HOME/.pub-cache/bin" |
| Generate | protoc --dart_out=grpc:lib/src/generated -Iprotos protos/file.proto |
Generated Files
| File | Contents |
|---|---|
file.pb.dart | Message classes |
file.pbgrpc.dart | Client stub and server base class |
file.pbenum.dart | Enum classes (if any) |
RPC Types
| Type | Proto Syntax | Server Method |
|---|---|---|
| Unary | rpc M (Req) returns (Res) | Future<Res> m(call, req) |
| Server streaming | rpc M (Req) returns (stream Res) | Stream<Res> m(call, req) |
| Client streaming | rpc M (stream Req) returns (Res) | Future<Res> m(call, Stream<Req>) |
| Bidirectional | rpc M (stream Req) returns (stream Res) | Stream<Res> m(call, Stream<Req>) |
Channel Options
| Option | Purpose |
|---|---|
ChannelCredentials.insecure() | Plaintext, local only |
ChannelCredentials.secure() | TLS with certificates |
CallOptions(metadata: ...) | Headers, tokens |
CallOptions(timeout: ...) | Deadline |
CallOptions(cancellation: ...) | Cancellation token |
Best Practices
✅ Do This:
// Use the generated base class
class MyService extends MyServiceBase { ... }
// Use secure credentials in production
ChannelCredentials.secure()
// Shut down the channel
await channel.shutdown();
// Use ServiceCall for context
call.clientMetadata // read headers
call.cancelled // check cancellation
❌ Don’t Do This:
// Use insecure credentials in production
ChannelCredentials.insecure() // plaintext
// Forget to regenerate after proto changes
// The generated code will not match the contract
// Ignore the ServiceCall context
// The client's deadline and cancellation are lost
// Reuse field numbers in the proto
// Breaks backward compatibility
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
protoc not found | Plugin not in PATH | export PATH="$PATH:$HOME/.pub-cache/bin" |
| Generated code stale | Proto changed without regeneration | Re-run protoc |
| Connection refused | Server not running or wrong port | Check server.port |
| Channel hangs on exit | Not shut down | await channel.shutdown() |
| Streaming error | Not handling stream errors | try/catch around await for |
Real-World Examples
1. Unary RPC
rpc GetUser (UserRequest) returns (UserResponse);
2. Server streaming
rpc ListUsers (Empty) returns (stream User);
3. Client streaming
rpc Upload (stream Chunk) returns (UploadResult);
4. Bidirectional streaming
rpc Chat (stream Message) returns (stream Message);
5. Server implementation
class UserService extends UserServiceBase {
@override
Future<UserResponse> getUser(ServiceCall call, UserRequest req) async { ... }
}
6. Client stub
final client = UserServiceClient(channel);
7. Secure channel
ChannelCredentials.secure()
8. Metadata
CallOptions(metadata: {'authorization': 'Bearer token'})
9. Timeout
CallOptions(timeout: Duration(seconds: 5))
10. Shutdown
await channel.shutdown();
Visual
gRPC Workflow
┌──────────────────────────────────────────────┐
│ .proto file │
│ │ │
│ ▼ │
│ protoc --dart_out=grpc:... │
│ │ │
│ ▼ │
│ generated: .pb.dart, .pbgrpc.dart │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ ▼ ▼ │
│ Server Client │
│ extends creates │
│ base stub │
│ │ │
│ ▼ │
│ HTTP/2 + Protocol Buffers │
└──────────────────────────────────────────────┘
Server Streaming
┌──────────────────────────────────────────────┐
│ Client sends one request │
│ │ │
│ ▼ │
│ Server returns Stream<Response> │
│ │ │
│ ▼ │
│ Client awaits for each response │
│ until the stream closes │
└──────────────────────────────────────────────┘
Channel Lifecycle
┌──────────────────────────────────────────────┐
│ ClientChannel('localhost', port: 50051) │
│ │ │
│ ▼ │
│ HTTP/2 connection established │
│ │ │
│ ▼ │
│ RPC calls │
│ │ │
│ ▼ │
│ channel.shutdown() │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Library | package:grpc |
| Contract | .proto file |
| Code generation | protoc --dart_out=grpc:... |
| Server base | *ServiceBase |
| Client stub | *Client |
| Channel | ClientChannel |
| Transport | HTTP/2 |
| Serialization | Protocol Buffers |
| RPC types | Unary, server streaming, client streaming, bidirectional |
Key takeaways:
- The
.protofile is the contract. It defines the messages and the RPC methods, and both the client and the server generate their code from it. The contract must be regenerated after every change, or the generated code and the.protowill diverge . protocand the Dart plugin generate the stubs. The--dart_out=grpc:flag tells the plugin to generate the gRPC code, and the output directory determines where the files go. The plugin must be activated globally and in the PATH .- The server extends the generated base class. Each RPC method is overridden, and the
ServiceCallcontext carries the metadata, deadline, and cancellation signal . - The client creates a channel and a stub. The
ClientChannelmanages the HTTP/2 connection, and the*Clientstub provides a method for each RPC. The channel must be shut down when it is no longer needed. - gRPC supports four streaming patterns. The
streamkeyword in the.protofile selects the pattern, and the generated method signature reflects it:Futurefor unary,Streamfor server streaming, and both for bidirectional . - Insecure credentials are for local development only. Production channels use
ChannelCredentials.secure()with certificates or the platform’s trust store.
Remember: gRPC in Dart is a four-stage workflow: define the .proto, generate the code, implement the server, and call it from the client. The .proto file is the source of truth, and the generated code is the binding. The server extends the generated base class and the client uses the generated stub. The ServiceCall object is the context that carries the client’s metadata and the deadline. The ClientChannel is the connection, and it must be shut down. The library handles the HTTP/2 transport, the Protocol Buffers serialization, and the streaming, so the application can focus on the business logic. The result is a typed, efficient, and language-neutral communication layer for microservices.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!