Stage 1 · Code
Building Web Services
gRPC and Protocol Buffers
JSON over HTTP is not the only way services talk. Learn how `.proto` contracts, generated code, and streaming RPCs change the shape of service-to-service communication.
Why a Strict Contract Can Help
A JSON API is flexible because the payload is plain text. That flexibility is useful, but it also means more discipline is pushed onto humans and conventions. gRPC takes a different approach. Before the client and server talk, you write down the contract in a .proto schema file: what messages exist, what fields they contain, and what service methods are available.
Think of JSON like agreeing to meet and saying 'text me the details.' It works, but both sides must interpret the message carefully. A .proto file is more like sending a printed schedule with named slots. Everyone knows the shape up front. That up-front strictness is why gRPC feels especially comfortable in teams with many services and many languages.
With Protocol Buffers, the schema is not an afterthought. It is the shared source of truth from which Go code, client stubs, and server interfaces are generated.
Reading a `.proto` File
A .proto file declares messages and services. Messages are the data shapes. A service lists callable methods. The example below keeps the same deployment theme from the earlier JSON API lesson so you can compare the two worlds directly.
syntax = "proto3";
package academy.deployments.v1;
option go_package = "example.com/academy/deploymentsv1;deploymentsv1";
message Deployment {
string id = 1;
string service = 2;
string version = 3;
string environment = 4;
string status = 5;
}
message CreateDeploymentRequest {
string service = 1;
string version = 2;
string environment = 3;
}
message GetDeploymentRequest {
string id = 1;
}
message WatchDeploymentRequest {
string id = 1;
}
message DeploymentEvent {
string deployment_id = 1;
string status = 2;
string note = 3;
}
message LogChunk {
string deployment_id = 1;
bytes chunk = 2;
}
message UploadSummary {
int32 chunk_count = 1;
}
message ConsoleFrame {
string text = 1;
}
service DeploymentService {
rpc CreateDeployment(CreateDeploymentRequest) returns (Deployment);
rpc GetDeployment(GetDeploymentRequest) returns (Deployment);
rpc WatchDeployment(WatchDeploymentRequest) returns (stream DeploymentEvent);
rpc UploadLogs(stream LogChunk) returns (UploadSummary);
rpc SyncConsole(stream ConsoleFrame) returns (stream ConsoleFrame);
}You can read this file almost like an interface plus data definitions. It names the fields, fixes their types, and declares exactly what methods the service exposes.
Field numbers matter because Protocol Buffers use them on the wire. The name service helps humans, but the numeric tag = 2 is part of the encoding contract. That is one reason schema evolution in protobuf follows specific compatibility rules.
Unary and Streaming RPCs
A unary RPC feels most like a normal function call: one request goes in, one response comes back. That is the simplest shape, and it maps well to many CRUD-like operations. Streaming RPCs widen the model by allowing one or both sides to send multiple messages over the same RPC call.
| RPC shape | Mental model | Example from the schema |
|---|---|---|
| Unary | One request, one response | CreateDeployment |
| Server-streaming | Client asks once, server keeps sending updates | WatchDeployment |
| Client-streaming | Client sends many messages, server answers once at the end | UploadLogs |
| Bidirectional streaming | Both sides keep sending messages over one open call | SyncConsole |
That streaming support is a major reason teams choose gRPC. You can model ongoing conversations without inventing your own polling loop or pushing everything through ad hoc HTTP endpoints. The trade-off is that the tooling and runtime model become more specialized than a plain JSON API.
The Code-Generation Workflow
With JSON REST, you often write the handler code by hand and then maybe document it. With gRPC, the order is flipped. You write the .proto file first. Then code generation tools create Go types for the messages and Go interfaces or stubs for the service. Your job becomes implementing the generated server interface and calling the generated client methods.
1. Write api/deployments.proto
2. Run the protobuf generators for Go
3. Get generated message types and client/server stubs
4. Implement the generated Go server interface
5. Register that server with a gRPC runtime
6. Call the generated client methods from other servicesThe generators remove a lot of repetitive transport code. You do not hand-write request parsing for every method; the schema and generated code shoulder that burden.
When gRPC Is Worth It
gRPC is not automatically better than JSON REST. It is a different trade-off. If you need a public browser-friendly API that is easy to inspect with curl and straightforward for many external consumers, JSON over HTTP is often the simpler choice. If you run many internal services, want strict contracts, care about efficient binary payloads, or need native streaming, gRPC becomes much more attractive.
| Choose JSON REST when... | Choose gRPC when... |
|---|---|
| You want broad human readability and easy debugging with common HTTP tools | You want a schema-first contract with generated clients and servers |
| Your API is public-facing or heavily browser-oriented | Your communication is mostly service-to-service inside controlled environments |
| Simple request-response endpoints are enough | Streaming or long-lived RPCs are a core requirement |
| The team benefits from fewer moving parts and less specialized tooling | The team benefits from stronger type safety across multiple languages and smaller binary payloads |
The real question is: what communication shape does this system need, and what complexity budget does the team have? A simpler JSON API can be the better engineering decision when it already fits the problem well.
Quiz and Practice
What is the main job of a `.proto` file in a gRPC system?
Hands-On Project
Implement `RPCShape`. If neither side streams, return `unary`. If only the server streams, return `server-streaming`. If only the client streams, return `client-streaming`. If both stream, return `bidirectional-streaming`.
Summary and Key Takeaways
- Protocol Buffers let you define message and service contracts before client and server code is written.
- gRPC can generate Go types and stubs from that contract, reducing repetitive transport plumbing.
- Unary RPCs look like one request and one response; streaming RPCs keep the conversation open longer.
- gRPC is especially compelling for internal service-to-service systems that need strict contracts, efficiency, or streaming.
- A simpler JSON REST API is often still the right answer when public compatibility and operational simplicity matter more.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.