08 — Protocol Buffers and API design

Goal

Write one service contract and generate Go and Dart types from it.

Step 1: Create the proto file

From field-notes, create api/fieldnotes/v1/task.proto:

syntax = "proto3";

package fieldnotes.v1;

option go_package = "github.com/example/field-notes/gen/go/fieldnotes/v1;fieldnotesv1";

import "google/protobuf/timestamp.proto";

enum TaskStatus {
  TASK_STATUS_UNSPECIFIED = 0;
  TASK_STATUS_TODO = 1;
  TASK_STATUS_IN_PROGRESS = 2;
  TASK_STATUS_DONE = 3;
}

message Task {
  string id = 1;
  string project_id = 2;
  string title = 3;
  string description = 4;
  TaskStatus status = 5;
  google.protobuf.Timestamp created_at = 6;
  google.protobuf.Timestamp updated_at = 7;
}

message GetTaskRequest {
  string id = 1;
}

message CreateTaskRequest {
  string project_id = 1;
  string title = 2;
  string description = 3;
}

message CreateTaskResponse {
  Task task = 1;
}

service TaskService {
  rpc GetTask(GetTaskRequest) returns (Task);
  rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse);
}

The package is versioned as v1. Tag numbers are part of the wire format: never renumber or reuse a field tag. Zero is reserved as the unspecified enum value so an omitted status is distinguishable from intentional states.

Protocol Buffers are a compact binary message format and an interface definition language. Each field has a number used on the wire, so a client can often ignore fields it does not know while still reading the rest of a message. A service declaration defines RPC methods, and each method names its request and response message. Proto names are converted into idiomatic Go and Dart identifiers by their generators.

The go_package option tells the Go generator what import path/package to emit. The protobuf package gives the service a stable namespace independent of its filesystem folder. These are related but separate names. Timestamp uses Google's standard well-known type so both generated runtimes share a recognized time representation.

Step 2: Generate Go code

From field-notes:

mkdir gen\go\fieldnotes\v1 -Force
protoc -I api --go_out=. --go_opt=module=github.com/example/field-notes --go-grpc_out=. --go-grpc_opt=module=github.com/example/field-notes api/fieldnotes/v1/task.proto

The output directory is determined by go_package: gen/go/fieldnotes/v1. The Go module path must match the --go_opt=module value. If you choose a different module name, update both consistently.

Add runtime dependencies from the repository root:

go get google.golang.org/grpc google.golang.org/protobuf

Step 3: Generate Dart code

Create the Flutter app if you skipped the prior chapter, then add dependencies:

cd flutter_app
flutter pub add grpc protobuf
cd ..
mkdir flutter_app\lib\src\generated -Force
protoc -I api --dart_out=grpc:flutter_app/lib/src/generated api/fieldnotes/v1/task.proto

You should see generated .pb.dart and .pbgrpc.dart files (plus enum/JSON files). Do not edit these files; edit .proto and regenerate.

Step 4: Inspect the generated API

Find the TaskServiceClient in Dart and the TaskServiceServer interface in Go. Identify the generated TaskStatus enum in each language. The generated names follow language-specific naming conventions, but the same field tags and RPC names define the shared wire contract.

Checkpoint

Change a comment in the proto, rerun both generation commands, and confirm both languages still compile. Add a priority integer field using the next available tag without changing existing tags.

Important compatibility rules

  • Never change a field's meaning or wire type in place.
  • Reserve both tag numbers and names when deleting fields.
  • Add new optional fields in a backward-compatible way.
  • Treat zero/default values deliberately; use explicit presence or update masks when “not provided” differs from “set to empty/zero”.
  • Keep the same proto contract checked in so another developer can reproduce generated code.

Generated files are derived artifacts: changing them by hand would be lost the next time generation runs. Update the .proto, rerun the compiler plugins, and then update both implementations. This makes API review focus on the contract change instead of an accidental mismatch between generated client and server code.

results matching ""

    No results matching ""