gRPC with Node.js
gRPC (Google Remote Procedure Call) is a high-performance RPC framework that uses Protocol Buffers (protobuf) as its interface definition language and serialization format, and HTTP/2 as the transport. Where REST sends JSON over HTTP/1.1, gRPC sends compact binary frames over a multiplexed HTTP/2 connection — typically 5–10× faster to serialize and 30–50% smaller on the wire. More importantly, you define your API as a typed .proto schema, and the framework generates strongly-typed client and server stubs in any language. This page covers the gRPC model, writing a .proto contract, and building both a server and client in Node using the @grpc/grpc-js and @grpc/proto-loader packages.
gRPC vs REST
Dimension | REST/JSON | gRPC/Protobuf |
|---|---|---|
Transport | HTTP/1.1 (one request per connection) | HTTP/2 (multiplexed streams, one connection) |
Serialization | JSON (text, human-readable) | Protocol Buffers (binary, compact, fast) |
Schema | Optional (OpenAPI) | Required |
Code generation | Optional | First-class — generates client/server stubs |
Streaming | Limited (SSE, WebSocket workarounds) | Built-in: client streaming, server streaming, bidirectional |
Browser support | Native | Requires gRPC-Web proxy |
Debugging | Easy (curl, browser devtools) | Harder (binary — needs grpcurl or Postman) |
Best for | Public APIs, browser clients | Internal service-to-service calls, microservices |
Defining a .proto contract
// proto/user.proto
syntax = "proto3";
package user;
// Service definition: each rpc = one method
service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc CreateUser (CreateUserRequest) returns (UserResponse);
rpc ListUsers (ListUsersRequest) returns (stream UserResponse); // server streaming
}
// Messages: typed, numbered fields (numbers must never change after deployment)
message GetUserRequest {
string id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message ListUsersRequest {
int32 page = 1;
int32 pageSize = 2;
}
message UserResponse {
string id = 1;
string name = 2;
string email = 3;
}Installing gRPC packages
npm install @grpc/grpc-js @grpc/proto-loader
Server implementation
import path from 'node:path'
import * as grpc from '@grpc/grpc-js'
import * as protoLoader from '@grpc/proto-loader'
// Load the .proto at runtime (no code generation step required with proto-loader)
const packageDef = protoLoader.loadSync(path.join(__dirname, '../proto/user.proto'), {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
})
const proto = grpc.loadPackageDefinition(packageDef) as any
const UserService = proto.user.UserService
// In-memory store for demo
const users: Record<string, { id: string; name: string; email: string }> = {}
// Implement each rpc method:
const serviceImpl = {
GetUser(
call: grpc.ServerUnaryCall<{ id: string }, unknown>,
callback: grpc.sendUnaryData<unknown>
) {
const user = users[call.request.id]
if (!user) {
return callback({ code: grpc.status.NOT_FOUND, message: 'User not found' })
}
callback(null, user)
},
CreateUser(
call: grpc.ServerUnaryCall<{ name: string; email: string }, unknown>,
callback: grpc.sendUnaryData<unknown>
) {
const id = String(Date.now())
const user = { id, ...call.request }
users[id] = user
callback(null, user)
},
ListUsers(call: grpc.ServerWritableStream<{ page: number; pageSize: number }, unknown>) {
// Server streaming: write multiple responses then end the stream
const all = Object.values(users)
for (const user of all) {
call.write(user)
}
call.end()
},
}
// Start the server:
const server = new grpc.Server()
server.addService(UserService.service, serviceImpl)
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), (err, port) => {
if (err) throw err
console.log(`gRPC server listening on port ${port}`)
})Client implementation
import path from 'node:path'
import * as grpc from '@grpc/grpc-js'
import * as protoLoader from '@grpc/proto-loader'
import { promisify } from 'node:util'
const packageDef = protoLoader.loadSync(path.join(__dirname, '../proto/user.proto'), {
keepCase: true, longs: String, enums: String, defaults: true, oneofs: true,
})
const proto = grpc.loadPackageDefinition(packageDef) as any
const client = new proto.user.UserService(
'localhost:50051',
grpc.credentials.createInsecure() // use createSsl() in production
)
// Promisify unary calls for async/await usage:
const getUser = promisify(client.GetUser.bind(client))
const createUser = promisify(client.CreateUser.bind(client))
async function demo() {
// Unary call:
const created = await createUser({ name: 'Alice', email: 'alice@example.com' })
console.log('Created:', created)
const fetched = await getUser({ id: created.id })
console.log('Fetched:', fetched)
// Server streaming:
const stream = client.ListUsers({ page: 1, pageSize: 10 })
stream.on('data', (user: unknown) => console.log('User:', user))
stream.on('end', () => console.log('Stream ended'))
stream.on('error', (err: Error) => console.error('Stream error:', err))
}Error handling and status codes
// Server: return status codes via the callback error argument:
callback({ code: grpc.status.NOT_FOUND, message: 'User not found' })
callback({ code: grpc.status.INVALID_ARGUMENT, message: 'Email is required' })
callback({ code: grpc.status.INTERNAL, message: 'Database error' })
callback({ code: grpc.status.ALREADY_EXISTS, message: 'Email already registered' })
callback({ code: grpc.status.UNAUTHENTICATED, message: 'Missing credentials' })
callback({ code: grpc.status.PERMISSION_DENIED, message: 'Insufficient permissions' })
// Client: check the status code to distinguish error types:
try {
const user = await getUser({ id: '999' })
} catch (err: any) {
if (err.code === grpc.status.NOT_FOUND) console.log('No such user')
if (err.code === grpc.status.UNAVAILABLE) console.log('Server down — retry later')
}TLS in production
import fs from 'node:fs'
// Server with TLS:
const serverCreds = grpc.ServerCredentials.createSsl(
fs.readFileSync('certs/ca.crt'),
[{ cert_chain: fs.readFileSync('certs/server.crt'), private_key: fs.readFileSync('certs/server.key') }],
false // true = require client certificates (mutual TLS)
)
server.bindAsync('0.0.0.0:50051', serverCreds, ...)
// Client with TLS:
const clientCreds = grpc.credentials.createSsl(fs.readFileSync('certs/ca.crt'))
const client = new proto.user.UserService('api.internal:50051', clientCreds)