Skip to content
Blog

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 — curl cannot speak gRPC; you need grpcurl or 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

  1. Define protos in a dedicated directory; treat them as the source of truth and review changes like API changes.
  2. Generate clients in CI so nobody hand-edits generated files.
  3. Use StatusCode enums for control flow; never string-match error messages.
  4. Always set deadlines on calls from UI-triggered actions.
  5. 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

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.