Defining Services
The syntax
Section titled “The syntax”A service is declared with the service keyword, and each method with rpc. The shape is always the same: a name, one request message in parentheses, and returns with one response message.
syntax = "proto3";package user.v1;
service UserService { rpc GetUser(GetUserRequest) returns (User); rpc CreateUser(CreateUserRequest) returns (User); rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);}Read it as a menu of calls: this service offers three methods, each with a typed input and a typed output. That’s the entire API surface — no URLs, no verbs, no query strings.
One request and one response message per method
Section titled “One request and one response message per method”Notice that every method takes its own request message and returns its own response message — even GetUser, which conceptually just needs an id. This is deliberate, not verbose for its own sake:
// Do this — dedicated messagesrpc GetUser(GetUserRequest) returns (User);
message GetUserRequest { int64 id = 1;}If tomorrow you need to add “include deleted users” or “which fields to return”, you add a field to GetUserRequest — a safe, backward-compatible change. Had the method taken a bare int64, you’d have no place to grow, and changing the signature would break every client. Wrapper messages give every method room to evolve. The Designing Methods lesson returns to this rule in depth.
Packages and naming
Section titled “Packages and naming”The package declaration namespaces your types so they don’t collide with types from other .proto files. It also maps to a namespace in the generated code (a Go package, a Python module prefix, and so on).
package user.v1;Two conventions matter:
- Reverse-domain or product-scoped names keep packages globally unique:
acme.user.v1,payments.billing.v2. - Service names use PascalCase (
UserService), methods use PascalCase (GetUser), and fields use snake_case (user_id). The code generators translate these into each language’s idioms automatically.
Put the version in the package
Section titled “Put the version in the package”This is the single most important design habit in this lesson: the API version lives in the package path, as v1, v2, and so on.
flowchart TB v1["package user.v1 UserService"] --> old["existing clients keep calling v1"] v2["package user.v2 UserService (redesigned)"] --> new["new clients adopt v2"] note["both run side by side until v1 is retired"]
Because user.v1.UserService and user.v2.UserService are distinct types with distinct wire identities, you can ship a breaking redesign as v2 while v1 keeps serving existing clients. There is no forced flag-day migration. When every client has moved, you retire v1. Bake the version in from day one — retrofitting it later is painful.