gRPC in Flutter: Typed RPCs Between Your App and Backend
REST plus hand-written models gets old fast. gRPC for Flutter gives you protobuf-defined contracts, generated clients, streaming, and deadlines — across mobile and web.
Published on • October 13, 2026
AI Assistant

Most Flutter apps talk to their backend over REST: JSON strings, hand-written model classes, fromJson factories that throw at runtime when a field renames. It works, until the contract drifts and a “which field is it this week” bug ships to production.
gRPC flips the workflow: you define the contract once in a .proto file, and both your Dart client and your server get generated, type-checked code from it. The grpc package — published by the Dart team on pub.dev — brings the full gRPC model to Flutter, including streaming and the browser via gRPC-Web.
What you get from the package
The grpc Dart package implements the gRPC protocol over HTTP/2, with support for:
- Unary RPCs — plain request/response, the familiar shape.
- Server streaming — one request, many server responses.
- Client streaming — many requests, one response.
- Bidirectional streaming — both directions flow independently.
On the web, where browsers cannot open raw HTTP/2 sockets, the same generated stubs work through package:grpc/grpc_web.dart — the gRPC-Web protocol rides on a normal fetch/XHR connection. Your app code is nearly identical across platforms; only the channel differs.
The contract-first workflow
Write the service definition:
syntax = "proto3";
package notes;
service NoteService {
rpc GetNote (NoteRequest) returns (Note);
rpc WatchNotes (WatchRequest) returns (stream Note); // server streaming
rpc CreateNote (Note) returns (NoteResponse);
}
message NoteRequest { string id = 1; }
message Note {
string id = 1;
string title = 2;
string body = 3;
int64 updated_at = 4;
}
message NoteResponse { string id = 1; }
message WatchRequest { string folder = 1; }
Generate Dart with the protoc_plugin protoc plugin:
dart run protoc_plugin protos/notes.proto
This emits notes.pb.dart (messages) and notes.pbgrpc.dart (the NoteServiceClient and the abstract NoteServiceBase your server implements). The messages come with toJson/fromJson-equivalent protobuf serialization, equality, and copyWith — the boilerplate you would have written by hand simply disappears, and it cannot silently drift from the server.
Connecting from a Flutter client
final channel = ClientChannel(
'api.example.com',
port: 443,
options: const ChannelOptions(
credentials: ChannelCredentials.secure(),
),
);
final client = NoteServiceClient(channel);
// unary
final note = await client.getNote(NoteRequest()..id = '42');
// server streaming — the part REST cannot do cleanly
final stream = client.watchNotes(WatchRequest()..folder = 'inbox');
await for (final n in stream) {
_controller.add(n);
}
On web, swap the channel:
import 'package:grpc/grpc_web.dart';
final channel = GrpcWebClientChannel.xhr(Uri.parse('https://api.example.com'));
Everything downstream — client.getNote(...), the streaming loops — is unchanged.
Errors, deadlines, and metadata
gRPC has a real error model instead of ad-hoc HTTP status codes. Every failure surfaces as a GrpcError with a canonical code (notFound, permissionDenied, unavailable, deadlineExceeded…), a message, and optional details:
try {
await client.getNote(request).timeout(const Duration(seconds: 5));
} on GrpcError catch (e) {
if (e.code == StatusCode.notFound) {
// show empty state
} else if (e.code == StatusCode.deadlineExceeded) {
// retry or surface timeout UI
} else {
rethrow;
}
}
Deadlines are first-class: pass a timeout and the client enforces it, cancelling the call and reporting deadlineExceeded — no more futures that hang forever. Call metadata (headers) flows both ways, which is where you attach auth tokens:
final resp = await client.getNote(
request,
options: CallOptions(metadata: {'authorization': 'Bearer $token'}),
);
When REST still wins
gRPC is not a free upgrade:
- Public/open APIs and third-party integrations usually expect REST and JSON — you cannot force external partners onto your proto.
- Caching and intermediaries are easier with HTTP verbs and JSON bodies.
- Debuggability —
curlcannot speak gRPC; you needgrpcurlor a reflection-enabled tool. - Server support — your backend must expose gRPC (most modern frameworks do, but it is a constraint).
The pragmatic pattern is internal RPC via gRPC (app → your backend), with REST or GraphQL at the edges where the outside world touches you.
Getting started on your team
- Define protos in a dedicated directory; treat them as the source of truth and review changes like API changes.
- Generate clients in CI so nobody hand-edits generated files.
- Use
StatusCodeenums for control flow; never string-match error messages. - Always set deadlines on calls from UI-triggered actions.
- Keep a
grpcurl-friendly dev environment (enable server reflection in debug) so you can probe the API without a Flutter rebuild.
Primary sources and further reading
grpcpackage on pub.dev — the Dart gRPC implementation for server, CLI, and Flutter, including gRPC-Webprotoc_pluginon pub.dev — the protoc Dart code generator- gRPC docs — concepts — services, RPCs, and streaming semantics
- gRPC-Web introduction — how browser clients fit in
Typed contracts, streaming that is not a hack, and deadlines you can trust — gRPC in Flutter is one of the few dependencies that pays for itself the first time a backend field rename breaks a build instead of a customer.