gRPC Services

Reviewed & published by Brayan K

gRPC is a high-performance way for services to call each other. You define a service contract once in a Protocol Buffers .proto file, and both the server and client speak it over HTTP/2 with compact binary messages instead of JSON.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll write a .proto definition, then build a Node.js server and client with @grpc/grpc-js and @grpc/proto-loader, covering both unary and streaming RPCs — and see why teams pick gRPC over REST for internal traffic.

What You'll Learn in This Lesson

1️⃣ The Contract: a .proto File

Everything in gRPC starts with the contract. A .proto file declares services (groups of RPCs) and messages (typed records). Each message field gets a unique number — that tag, not the name, is what travels on the wire, which is why Protobuf is so compact.

// greeter.proto — the CONTRACT both server and client share.
// Protocol Buffers (protobuf) is a compact, strongly typed binary format.
syntax = "proto3";

package greeter;

// A service groups together remote procedure calls (RPCs).
service Greeter {
  // Unary RPC: one request in, one response out.
  rpc SayHello (HelloRequest) returns (HelloReply);

  // Server-streaming RPC: one request in, a STREAM of replies out.
  rpc Countdown (CountRequest) returns (stream CountReply);
}

// Messages are typed records. Each field has a unique NUMBER (the tag)
// that identifies it on the wire — never reuse or renumber tags.
message HelloRequest { string name = 1; }
message HelloReply   { string message = 1; }

message CountRequest { int32 from = 1; }
message CountReply   { int32 value = 1; }

Notice the stream keyword on Countdown's return type — that's what makes it a server-streaming RPC. The same file is shared by server and client, so both always agree on the shapes.

2️⃣ Building the Server

Load the .proto with proto-loader, implement each RPC, register them with addService, and bind to a port. A unary handler answers with callback(null, response); a streaming handler calls call.write(...) repeatedly and then call.end().

// install: npm install @grpc/grpc-js @grpc/proto-loader
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";

// 1) Load the .proto at runtime and turn it into callable objects.
const packageDef = protoLoader.loadSync("greeter.proto");
const proto = grpc.loadPackageDefinition(packageDef).greeter;

// 2) Implement each RPC. A unary handler gets (call, callback);
//    you call callback(error, response) — null error means success.
function sayHello(call, callback) {
  const name = call.request.name;
  callback(null, { message: "Hello, " + name + "!" });
}

// 3) A server-streaming handler writes many messages, then ends the stream.
function countdown(call) {
  for (let n = call.request.from; n >= 0; n--) {
    call.write({ value: n });   // push each response onto the stream
  }
  call.end();                   // close the stream when done
}

// 4) Build the server, register the service implementation, and listen.
const server = new grpc.Server();
server.addService(proto.Greeter.service, { sayHello, countdown });
server.bindAsync(
  "0.0.0.0:50051",
  grpc.ServerCredentials.createInsecure(), // TLS omitted for the demo
  () => console.log("gRPC server listening on :50051")
);

The error-first callback(error, response) is the standard Node convention: pass null as the error on success, or a grpc.status error to signal failure. The client receives whichever you send.

3️⃣ Calling It from a Client

The client loads the same .proto and creates a stub — an object whose methods look local but actually make remote calls. A unary call passes a request and reads the response in a callback; a streaming call returns an event-emitting stream you read with on("data") and on("end").

import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";

const packageDef = protoLoader.loadSync("greeter.proto");
const proto = grpc.loadPackageDefinition(packageDef).greeter;

// A client "stub" makes the remote service look like a local object.
const client = new proto.Greeter(
  "localhost:50051",
  grpc.credentials.createInsecure()
);

// Unary call: pass a request, get one response in the callback.
client.sayHello({ name: "Ada" }, (err, reply) => {
  if (err) throw err;
  console.log(reply.message);          // -> "Hello, Ada!"
});

// Server-streaming call: returns a stream you read with events.
const stream = client.countdown({ from: 3 });
stream.on("data", (reply) => console.log("tick", reply.value)); // 3,2,1,0
stream.on("end", () => console.log("done"));

Because the contract is shared, the stub already knows the exact request and reply shapes. No URL parsing, no manual JSON — you call client.sayHello(...) like a function and get a typed reply back.

Your turn. Fill in the two ___ blanks to complete a working unary round-trip.

// 🎯 YOUR TURN — fill in the ___ blanks to finish a unary call.

// CONTRACT (greeter.proto):
//   rpc SayHello (HelloRequest) returns (HelloReply);
//   message HelloReply { string message = 1; }

// SERVER handler — return a successful response:
function sayHello(call, callback) {
  callback(___, { message: "Hi " + call.request.name }); // 👉 what goes first?
}

// CLIENT — read the reply field:
client.sayHello({ name: "Grace" }, (err, reply) => {
  console.log(reply.___);  // 👉 which field name from HelloReply?
});

// ✅ The first blank is null (no error = success). The second blank is
//    message, the field declared in HelloReply.

Mini-Challenge: Add a Streaming RPC

Extend the contract with a new server-streaming RPC, for example rpc Fibonacci (CountRequest) returns (stream CountReply);. Implement the handler so it call.write()s each Fibonacci number up to the requested count, then call.end()s. On the client, read it with stream.on("data", ...). Confirm the values arrive one at a time over a single HTTP/2 connection — that's the streaming power that REST can't match cleanly.

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — gRPC in Node

GoalAPI
Load a .protoprotoLoader.loadSync(path)
Build callable objectsgrpc.loadPackageDefinition(def)
Register handlersserver.addService(svc, impl)
Answer a unary RPCcallback(null, response)
Stream from servercall.write(msg); call.end()
Create a client stubnew proto.Greeter(addr, creds)

🎉 Lesson Complete!

Practice quiz

What does gRPC use to define services and message types?

  • Protocol Buffers (.proto files)
  • OpenAPI/Swagger YAML
  • JSON Schema
  • GraphQL SDL

Answer: Protocol Buffers (.proto files). gRPC uses Protocol Buffers .proto files as the contract for services and messages.

Which transport protocol does gRPC run over?

  • HTTP/1.1
  • WebSocket
  • HTTP/2
  • raw TCP with no framing

Answer: HTTP/2. gRPC is built on HTTP/2, which enables multiplexing and streaming over one connection.

In a .proto file, how do you give each field its position on the wire?

  • Field positions are automatic and invisible
  • By a unique field number after the equals sign, like name = 1
  • By its order in the file only
  • By a JSON key

Answer: By a unique field number after the equals sign, like name = 1. Each field has an explicit tag number (name = 1) that identifies it in the binary encoding.

Which Node.js packages are commonly used to run gRPC?

  • axios and ws
  • amqplib and protobufjs only
  • express and body-parser
  • @grpc/grpc-js and @grpc/proto-loader

Answer: @grpc/grpc-js and @grpc/proto-loader. @grpc/grpc-js is the pure-JS gRPC library; @grpc/proto-loader loads .proto files at runtime.

What is a unary RPC?

  • A single request that returns a single response
  • A single request that returns a stream of responses
  • A stream of requests returning a single response
  • A stream of requests returning a stream of responses

Answer: A single request that returns a single response. Unary is the simplest RPC: one request message in, one response message out.

Which RPC type lets a server push many messages for one request, like a live feed?

  • Client streaming
  • Server streaming
  • There is no streaming in gRPC
  • Unary

Answer: Server streaming. Server streaming sends one request and the server returns a stream of response messages.

On the server, how do you attach a method implementation to a service?

  • channel.consume(method, handler)
  • server.use(handler)
  • app.get('/method', handler)
  • server.addService(serviceDef, { methodName: handler })

Answer: server.addService(serviceDef, { methodName: handler }). You register handlers with server.addService(serviceDefinition, implementationObject).

In a unary handler, how do you return a successful response to the client?

  • throw responseObject
  • ctx.send(responseObject)
  • callback(null, responseObject)
  • return res.json(value)

Answer: callback(null, responseObject). Node gRPC unary handlers use a callback: callback(error, response), with null error on success.

Compared with REST and JSON, what is a key advantage of gRPC?

  • It is easier to call from a browser address bar
  • Compact binary Protobuf messages and a strongly typed, code-generated contract
  • It requires no schema at all
  • It only works over HTTP/1.1

Answer: Compact binary Protobuf messages and a strongly typed, code-generated contract. Protobuf is smaller and faster to parse than JSON, and the .proto gives a typed, generated contract.

What does proto-loader's loadSync followed by loadPackageDefinition produce?

  • A usable gRPC object with the service constructors and client stubs
  • A raw JSON string of the proto
  • An HTTP route table
  • A database schema

Answer: A usable gRPC object with the service constructors and client stubs. loadSync parses the .proto and loadPackageDefinition turns it into callable service/client objects.

Continue this course

Frequently asked questions

What is the difference between gRPC and REST?

REST is an architectural style that usually sends JSON over HTTP/1.1 with URLs and verbs like GET and POST; it is human-readable and trivial to call from a browser or curl. gRPC is a binary RPC framework: you define a service in a .proto file, messages are encoded as compact Protocol Buffers, and calls travel over HTTP/2. The big practical differences are that gRPC gives you a strongly typed, code-generated contract shared by client and server, smaller and faster messages, and first-class streaming in both directions. REST is friendlier for public, browser-facing APIs; gRPC shines for fast, typed service-to-service communication inside a backend.

Why does each Protobuf field need a number?

The field number, written as name = 1, is the field's identifier on the wire. Protocol Buffers does not store field names in the encoded bytes; it stores the tag number plus the value, which is what makes the format so compact. Because the number is the identity, you must never change or reuse a field's number once data has been written with it, or old and new code will misinterpret each other. You can freely add new fields with new numbers and rename a field in the .proto, since the name is only for your code, but the numbers form the stable contract that lets clients and servers built at different times stay compatible.

What are the four kinds of RPC in gRPC?

gRPC supports four call shapes. A unary RPC sends one request and gets one response, just like a normal function call. Server streaming sends one request and the server returns a stream of responses, which is ideal for things like a live price feed or paginated results pushed over time. Client streaming sends a stream of requests and gets a single response, useful for uploading or aggregating many items. Bidirectional streaming opens a stream in both directions at once so client and server can send messages independently, perfect for chat or real-time collaboration. All four are built on HTTP/2 streams over a single connection.

Why does the Node gRPC unary handler use a callback instead of returning a value?

Node gRPC follows the standard Node error-first callback convention. A unary handler receives (call, callback), and you signal completion by invoking callback(error, response). Passing null as the error and your response object means success; passing an error object (optionally a grpc.status code) means failure, which the client surfaces as the err argument. The library uses a callback rather than a return value so the same handler shape works whether you respond synchronously or after awaiting a database or another service. You can still use async functions inside the handler, you just call the callback when you have the result.