Node.js gRPC Server Tutorial
Build a typed, high-performance gRPC server in Node.js with protobuf definitions, streaming RPCs, and production-ready patterns.
What you'll learn
- ✓What gRPC offers over REST
- ✓Writing proto definitions
- ✓Implementing unary and streaming RPCs
- ✓Error and deadline handling
- ✓When to choose gRPC
Prerequisites
- •Node.js basics
- •Comfort reading schema definitions
What and Why
gRPC is a contract-first RPC framework built on HTTP/2 and Protocol Buffers. You define services and messages in a .proto file, generate or load bindings, and call methods between processes as if they were local. The wire format is binary, the schema is shared, and streaming is a first-class concept.
Why pick it over REST? Two reasons. The schema is enforced on both ends, which removes a whole class of integration bugs. And HTTP/2 plus protobuf gives you significantly lower latency and bandwidth for service-to-service communication, especially with bidirectional streaming.
Mental Model
Think of gRPC as four building blocks. Messages are typed data structures. Services are collections of methods. Methods come in four flavors: unary, server streaming, client streaming, and bidirectional streaming. Channels are long-lived HTTP/2 connections that multiplex many calls.
The proto file is the contract. The generated or dynamically loaded code is the binding. The server registers handlers, the client opens a channel, and the framework handles serialization, multiplexing, and deadlines.
Hands-on Example
A simple greeter service with one unary RPC and one server-streaming RPC.
// greeter.proto
syntax = "proto3";
package greeter;
service Greeter {
rpc Hello (HelloRequest) returns (HelloReply);
rpc Countdown (CountRequest) returns (stream CountReply);
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }
message CountRequest { int32 from = 1; }
message CountReply { int32 value = 1; }
// server.js
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const def = protoLoader.loadSync('greeter.proto');
const pkg = grpc.loadPackageDefinition(def).greeter;
const server = new grpc.Server();
server.addService(pkg.Greeter.service, {
Hello: (call, cb) => {
cb(null, { message: `Hello, ${call.request.name}` });
},
Countdown: (call) => {
let n = call.request.from;
const id = setInterval(() => {
if (n < 0) { clearInterval(id); call.end(); return; }
call.write({ value: n-- });
}, 200);
call.on('cancelled', () => clearInterval(id));
},
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log('gRPC server on 50051');
});
The countdown handler honors client cancellation, which keeps you from leaking timers when callers disconnect.
Client HTTP/2 stream Server
| --- unary request (protobuf) ---> |
| <-- unary response (protobuf) --- |
| |
| --- start server stream ------> |
| <-- message 1 ----------------- |
| <-- message 2 ----------------- |
| <-- END -------------------- | Common Pitfalls
Skipping deadlines makes every slow call propagate forever. Clients should always set a deadline; servers should check call.cancelled and bail out of expensive work.
Returning generic errors hides intent. Use grpc.status codes like NOT_FOUND or INVALID_ARGUMENT so clients can branch on them without parsing strings.
Sharing one channel across worker threads incorrectly can serialize calls. Create channels per process and let HTTP/2 multiplex within them.
Forgetting backpressure on streaming RPCs floods clients. Check call.write return values and listen for drain before writing more.
Practical Tips
Keep .proto files in a shared repo or package so every service uses the same generated code. Schema drift is the most common gRPC outage.
Wrap handlers in a small adapter that logs request id, status, and duration. gRPC interceptors are the right place for auth, tracing, and metrics.
Prefer many small RPCs over one giant one. Smaller messages are easier to evolve, cache, and parallelize.
For browser clients, expose a gRPC-Web proxy or use Connect-style transports. Native gRPC does not run in browsers.
Use TLS in production. Insecure credentials are fine for local development and dangerous everywhere else.
Wrap-up
gRPC gives you a typed contract, fast binary transport, and first-class streaming. Define proto files carefully, set deadlines on every call, return real status codes, and treat schema as a shared artifact. With those habits, gRPC becomes a powerful default for internal service-to-service calls where REST starts to feel both slow and loosely typed.
Related articles
- Go Go gRPC Tutorial
Build a gRPC service in Go from scratch: define protobufs, generate code, implement the server, write a client, and understand how streaming and interceptors fit together.
- Backend gRPC Introduction: Protobuf, Streaming, and Why
A working introduction to gRPC: protobuf schemas, code generation, the four RPC styles, deadlines, errors, and when to pick it over REST.
- Node.js Node.js Async Iterators Tutorial
Master async iterators in Node.js for streaming files, paginated APIs, and backpressure-aware data processing.
- Node.js Node EventEmitter Patterns
EventEmitter is the backbone of Node. Here are the patterns that make it useful in real systems and the mistakes that turn it into a footgun.