Designing Methods
The API you ship is forever
Section titled “The API you ship is forever”A method signature is a promise. Once clients depend on it, you can only change it in backward-compatible ways — or bump the whole package to v2. So the design habits below aren’t polish; they’re what let an API survive years of change without a painful migration.
Always wrap requests and responses
Section titled “Always wrap requests and responses”The single most valuable rule: every method gets its own request message and its own response message. Never take or return a bare scalar, and never reuse a domain message as a request.
// Do thisrpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
message DeleteUserRequest { int64 id = 1; }message DeleteUserResponse {} // empty today, has room tomorrow// Avoid thisrpc DeleteUser(int64) returns (google.protobuf.Empty);Why the wrappers, even when they’re empty today?
- Room to grow. Adding a
soft_deleteflag to the request, or adeleted_attimestamp to the response, is a safe additive change. A bare scalar or sharedEmptyhas nowhere to add it. - Clarity.
DeleteUserRequestdocuments intent better thanint64. - Consistency. Every method looks the same, so tooling and readers never have to special-case anything.
Naming conventions
Section titled “Naming conventions”Follow the conventions the code generators expect, and the generated client will read naturally in every language:
- Methods:
VerbNounin PascalCase —GetUser,CreateOrder,ListInvoices,BatchUpdateItems. Standard verbs (Get,List,Create,Update,Delete) set clear expectations. - Messages:
MethodNameRequest/MethodNameResponse—ListUsersRequest,ListUsersResponse. - Fields:
snake_case—user_id,page_size,created_at. Generators convert to each language’s casing.
Pagination: never return an unbounded list
Section titled “Pagination: never return an unbounded list”A ListUsers that returns all users will eventually return millions and fall over. Design list methods to page from the start, using the widely adopted token pattern:
message ListUsersRequest { int32 page_size = 1; // how many to return string page_token = 2; // opaque cursor from the previous response}
message ListUsersResponse { repeated User users = 1; string next_page_token = 2; // empty when there are no more}flowchart LR r1["ListUsers(page_size=50)"] --> p1["50 users + next_page_token=abc"] p1 --> r2["ListUsers(token=abc)"] r2 --> p2["next 50 + next_page_token=def"] p2 --> r3["... until token is empty"]
The client keeps passing the returned next_page_token back until it comes back empty. The token is opaque — the server decides what it encodes (an offset, a cursor, a keyset), and clients must never parse it. That freedom lets you change the pagination strategy later without touching a single client.
Idempotency: make retries safe
Section titled “Idempotency: make retries safe”Networks fail, and clients retry. If a retried CreatePayment charges the customer twice, that’s a design bug, not bad luck. Make mutating methods idempotent where you can — commonly with a client-supplied request id the server deduplicates:
message CreatePaymentRequest { Payment payment = 1; string request_id = 2; // client-generated; server ignores duplicates}The server records each request_id; a repeat returns the original result instead of charging again. Get/List are naturally idempotent; Delete usually is; Create needs help like this.