Defining Services and Messages with Protobuf IDL
Write .proto contracts and generate type-safe client and server stubs from them.
Defining Services and Messages with Protobuf IDL is a free Node.js Backend Development Bootcamp lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Node.js Backend Development Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why an IDL?
In a gRPC microservice, the contract comes before the code. You describe your data and your RPC methods in a language-neutral Interface Definition Language (IDL) called .proto, then generate type-safe stubs for Node.js, Go, Java, and more from that single file.
- One
.protofile is the single source of truth shared by client and server. - Protocol Buffers (protobuf) is both the IDL and the binary wire format.
- You never hand-write the serialization code — the compiler does it.
This lesson shows how to write .proto contracts and turn them into JavaScript client and server stubs.
Anatomy of a .proto file
Every modern .proto file starts by declaring the syntax version and a package. The package namespaces your symbols so two services can both define a User without colliding.
syntax = "proto3";— always use proto3 for new services.package— logical namespace, maps to a JS object path after generation.- The file groups
message(data shapes) andservice(RPC methods).
// user.proto
syntax = "proto3";
package users.v1;
// A data shape sent over the wire
message User {
string id = 1;
string email = 2;
bool active = 3;
}Messages and field numbers
A message is a record of typed fields. The number after the = is the field tag, not a default value. Tags are how protobuf identifies each field on the binary wire — they must be unique within a message and must never change once deployed.
- Field names can be renamed freely; tags must stay stable for backward compatibility.
- Tags 1-15 use a single byte, so reserve them for your most frequent fields.
- Scalar types:
string,bool,int32,int64,double,bytes.
message Product {
string id = 1; // tag 1 (1 byte on the wire)
string name = 2;
int32 stock = 3;
double price = 4;
bool discontinued = 5;
}proto3 to JavaScript type mapping
When you load a .proto with @grpc/proto-loader, each protobuf type maps to a JavaScript value. Knowing the mapping avoids surprises at runtime.
string,bool,int32,float,doublemap to JSstring,boolean,number.int64/uint64map to a string by default in JS (numbers can exceedNumber.MAX_SAFE_INTEGER).bytesbecomes aBuffer.- Unset proto3 scalars come back as their zero value (
"",0,false) — they are neverundefinedon the wire.
// What a decoded User object looks like in Node.js
const user = {
id: 'u_123', // string -> string
email: '', // unset string -> '' (zero value)
active: false, // unset bool -> false
loginCount: '0' // int64 -> string, not number!
};
console.log(typeof user.loginCount); // 'string'Defining a service
A service block lists the RPC methods clients can call. Each rpc takes exactly one request message and returns exactly one response message. Wrapping request/response in dedicated messages (rather than passing bare scalars) lets you add fields later without breaking the contract.
- Method names are
PascalCaseby convention. - Always define a request and a response message per method, even if one is empty.
message GetUserRequest { string id = 1; }
message GetUserResponse { User user = 1; }
service UserService {
// unary: one request -> one response
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (User);
}The four RPC kinds
The stream keyword on either side of an rpc declaration controls the streaming model. The same IDL keyword decides whether your generated handler receives a single value or a stream.
- Unary:
rpc Get(Req) returns (Res)— one in, one out. - Server streaming:
returns (stream Res)— server pushes many. - Client streaming:
(stream Req)— client uploads many. - Bidirectional:
(stream Req) returns (stream Res).
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc ListOrders(ListOrdersRequest) returns (stream Order);
rpc ImportOrders(stream Order) returns (ImportSummary);
rpc LiveOrders(stream OrderEvent) returns (stream OrderEvent);
}Enums, repeated, and nested messages
Beyond scalars, protobuf gives you composite shapes for real-world data.
enum— a closed set of values; the first member must be0and is the default.repeated— an ordered list; decodes to a JSArray.- Nested messages model structured sub-records.
Prefix enum members to avoid name clashes, since enum values share the enclosing scope.
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0; // required zero default
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_SHIPPED = 2;
}
message Order {
string id = 1;
OrderStatus status = 2;
repeated LineItem items = 3; // -> JS Array
}
message LineItem {
string sku = 1;
int32 qty = 2;
}Loading the contract in Node.js
In Node you generate stubs at runtime with @grpc/proto-loader plus @grpc/grpc-js. The loader parses the .proto and loadPackageDefinition turns it into a navigable JS object keyed by your package path.
keepCase: truepreserves field names exactly as written in the proto.longs: Stringkeeps 64-bit ints safe as strings.- The package path
users.v1becomesproto.users.v1.
const protoLoader = require('@grpc/proto-loader');
const grpc = require('@grpc/grpc-js');
const pkgDef = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(pkgDef);
const UserService = proto.users.v1.UserService;Implementing the server stub
The generated UserService gives you a service definition you register handlers against. Each unary handler receives (call, callback); call.request is your decoded request message, and you reply via the Node-style callback(err, response).
- Return a gRPC status by passing an error with a
codefromgrpc.status. - The response object must match the response message fields.
const server = new grpc.Server();
server.addService(UserService.service, {
GetUser(call, callback) {
const { id } = call.request;
const user = db.find(id);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: `User ${id} not found`,
});
}
callback(null, { user });
},
});Calling from the client stub
The same generated UserService is also a client constructor. You instantiate it with a target address and credentials, then call methods directly. Each unary call takes the request object and a Node-style callback.
credentials.createInsecure()for local/dev; use TLS in production.- The request and response are plain JS objects matching the proto messages.
const client = new UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
client.GetUser({ id: 'u_123' }, (err, res) => {
if (err) {
console.error(err.code, err.message);
return;
}
console.log(res.user.email);
});Evolving the contract safely
A contract is only useful if it can change without breaking deployed clients. proto3 makes additive evolution safe when you follow a few rules.
- Add new fields with new, never-before-used tag numbers — old clients ignore them.
- Never reuse or renumber tags; mark removed tags with
reserved. - Renaming a field is wire-compatible (tag is what matters), but breaks JSON/text usage.
- Use
reservedfor both removed tag numbers and names to prevent accidental reuse.
message User {
reserved 4, 5; // retired tags, never reuse
reserved "phone"; // retired field name
string id = 1;
string email = 2;
bool active = 3;
string display_name = 6; // safe additive change
}Quick Check
You need to remove the deprecated phone field (tag 4) from a deployed User message. What is the correct, backward-compatible way to do it?
Recap
You learned to design and consume gRPC contracts with Protobuf IDL:
- A
.protofile withsyntax = "proto3"and apackageis the single source of truth for client and server. messagedefines typed records; the number after each field is its stable wire tag, not a default.service+rpcdeclare methods; thestreamkeyword selects unary, server-, client-, or bidirectional streaming.enum,repeated, and nested messages model richer data;int64decodes to a JS string.- In Node,
@grpc/proto-loader+grpc.loadPackageDefinitiongenerate both the server service and the client constructor. - Evolve contracts additively and protect retired tags/names with
reserved.
Frequently asked questions
Is the “Defining Services and Messages with Protobuf IDL” lesson free?
Yes — the full text of “Defining Services and Messages with Protobuf IDL” is free to read here on the web, and the Node.js Backend Development Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Node.js Backend Development Bootcamp course, upgrade to CoddyKit PRO.
What will I learn in “Defining Services and Messages with Protobuf IDL”?
Write .proto contracts and generate type-safe client and server stubs from them. You practise Node.js Backend Development Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Node.js Backend Development Bootcamp?
No prior experience is required. Node.js Backend Development Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Defining Services and Messages with Protobuf IDL” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Node.js Backend Development Bootcamp lesson?
Yes. Every Node.js Backend Development Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Defining Services and Messages with Protobuf IDL
- Unary, Server, Client, and Bidirectional Streaming RPCs
- Interceptors, Deadlines, and Metadata
- Proto Evolution and Backward Compatibility