| |

Dart 77 🎯 Microservices and gRPC Services Implementation in Dart (package:grpc)

⏱️ Estimated read: 14 min | |

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

StepCommand
Activate plugindart pub global activate protoc_plugin
Update PATHexport PATH="$PATH:$HOME/.pub-cache/bin"
Generateprotoc --dart_out=grpc:lib/src/generated -Iprotos protos/file.proto

Generated Files

FileContents
file.pb.dartMessage classes
file.pbgrpc.dartClient stub and server base class
file.pbenum.dartEnum classes (if any)

RPC Types

TypeProto SyntaxServer Method
Unaryrpc M (Req) returns (Res)Future<Res> m(call, req)
Server streamingrpc M (Req) returns (stream Res)Stream<Res> m(call, req)
Client streamingrpc M (stream Req) returns (Res)Future<Res> m(call, Stream<Req>)
Bidirectionalrpc M (stream Req) returns (stream Res)Stream<Res> m(call, Stream<Req>)

Channel Options

OptionPurpose
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

PitfallWhy It HappensFix
protoc not foundPlugin not in PATHexport PATH="$PATH:$HOME/.pub-cache/bin"
Generated code staleProto changed without regenerationRe-run protoc
Connection refusedServer not running or wrong portCheck server.port
Channel hangs on exitNot shut downawait channel.shutdown()
Streaming errorNot handling stream errorstry/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

ItemValue
Librarypackage:grpc
Contract.proto file
Code generationprotoc --dart_out=grpc:...
Server base*ServiceBase
Client stub*Client
ChannelClientChannel
TransportHTTP/2
SerializationProtocol Buffers
RPC typesUnary, server streaming, client streaming, bidirectional

Key takeaways:

  • The .proto file 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 .proto will diverge .
  • protoc and 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 ServiceCall context carries the metadata, deadline, and cancellation signal .
  • The client creates a channel and a stub. The ClientChannel manages the HTTP/2 connection, and the *Client stub provides a method for each RPC. The channel must be shut down when it is no longer needed.
  • gRPC supports four streaming patterns. The stream keyword in the .proto file selects the pattern, and the generated method signature reflects it: Future for unary, Stream for 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.


Was this helpful?


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!