gRPC with Node.js — Complete Tutorial

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

gRPC is Google's RPC framework for building distributed systems. It uses Protocol Buffers and HTTP/2 for blazing-fast communication between services.

What is gRPC?

  • Protocol Buffers: Binary serialization format
  • HTTP/2: Multiplexed connections
  • Performance: 7x faster than REST
  • Streaming: Built-in request/response streaming
  • Language-agnostic: Generate code in any language

Setup

npm install @grpc/grpc-js @grpc/proto-loader
npm install --save-dev protobufjs

Defining Services with .proto Files

// user.proto
syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(Empty) returns (stream User);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc UpdateUser(UpdateUserRequest) returns (User);
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}

message GetUserRequest {
  string id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message UpdateUserRequest {
  string id = 1;
  string name = 2;
  string email = 3;
}

message Empty {}

Server Implementation

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

const packageDefinition = protoLoader.loadSync("user.proto");
const userProto = grpc.loadPackageDefinition(packageDefinition);

interface User {
  id: string;
  name: string;
  email: string;
}

const users: User[] = [
  { id: "1", name: "Alice", email: "alice@example.com" },
  { id: "2", name: "Bob", email: "bob@example.com" },
];

const userServiceImpl = {
  getUser: (call: any, callback: any) => {
    const user = users.find((u) => u.id === call.request.id);
    if (!user) {
      return callback(new Error("User not found"));
    }
    callback(null, user);
  },

  listUsers: (call: any) => {
    users.forEach((user) => {
      call.write(user);
    });
    call.end();
  },

  createUser: (call: any, callback: any) => {
    const newUser = {
      id: Date.now().toString(),
      name: call.request.name,
      email: call.request.email,
    };
    users.push(newUser);
    callback(null, newUser);
  },

  updateUser: (call: any, callback: any) => {
    const user = users.find((u) => u.id === call.request.id);
    if (!user) {
      return callback(new Error("User not found"));
    }
    user.name = call.request.name || user.name;
    user.email = call.request.email || user.email;
    callback(null, user);
  },
};

const server = new grpc.Server();
server.addService(userProto.UserService.service, userServiceImpl);

server.bindAsync(
  "127.0.0.1:50051",
  grpc.ServerCredentials.createInsecure(),
  (err, port) => {
    if (err) throw err;
    console.log(`gRPC server running on port ${port}`);
    server.start();
  }
);

Client Implementation

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

const packageDefinition = protoLoader.loadSync("user.proto");
const userProto = grpc.loadPackageDefinition(packageDefinition);

const client = new (userProto as any).UserService(
  "localhost:50051",
  grpc.credentials.createInsecure()
);

// Unary RPC
client.getUser({ id: "1" }, (err: any, response: any) => {
  if (err) {
    console.error(err);
  } else {
    console.log("User:", response);
  }
});

// Server streaming
const call = client.listUsers({});
call.on("data", (user: any) => {
  console.log("User:", user);
});
call.on("end", () => {
  console.log("Stream ended");
});

// Unary request
client.createUser(
  { name: "Charlie", email: "charlie@example.com" },
  (err: any, response: any) => {
    if (err) {
      console.error(err);
    } else {
      console.log("Created user:", response);
    }
  }
);

Streaming

# Bidirectional streaming
service ChatService {
  rpc Chat(stream Message) returns (stream Message);
}

message Message {
  string sender = 1;
  string text = 2;
}
// Server-side bidirectional streaming
const chatServiceImpl = {
  chat: (call: any) => {
    call.on("data", (message: any) => {
      console.log(`${message.sender}: ${message.text}`);
      // Echo back
      call.write({
        sender: "Server",
        text: `Echo: ${message.text}`,
      });
    });

    call.on("end", () => {
      call.end();
    });
  },
};

// Client-side bidirectional streaming
const call = client.chat();

call.write({ sender: "Client", text: "Hello" });
call.write({ sender: "Client", text: "How are you?" });

call.on("data", (response: any) => {
  console.log(`${response.sender}: ${response.text}`);
});

call.on("end", () => {
  console.log("Chat ended");
});

call.end();

Error Handling

client.getUser({ id: "999" }, (err: any, response: any) => {
  if (err) {
    console.error(`Error code: ${err.code}`);
    console.error(`Error message: ${err.message}`);

    switch (err.code) {
      case grpc.status.NOT_FOUND:
        console.error("User not found");
        break;
      case grpc.status.INTERNAL:
        console.error("Internal server error");
        break;
      case grpc.status.UNAUTHENTICATED:
        console.error("Not authenticated");
        break;
    }
  }
});

Metadata (Headers)

// Add metadata to request
const metadata = new grpc.Metadata();
metadata.add("authorization", "Bearer token123");

client.getUser({ id: "1" }, metadata, (err: any, response: any) => {
  console.log(response);
});

// Server-side: read metadata
const getUser = (call: any, callback: any) => {
  const auth = call.metadata.get("authorization")[0];
  console.log(`Authorization: ${auth}`);
  callback(null, { id: "1", name: "Alice", email: "alice@example.com" });
};

REST Gateway (Connect)

Use gRPC with REST clients:

import { ConnectRouter } from "@connectrpc/connect";

export const routes = (router: ConnectRouter) => {
  router.rpc(UserService, UserService.methods.getUser, getUser);
  router.rpc(UserService, UserService.methods.createUser, createUser);
};

// Accessible via HTTP:
// POST http://localhost:8080/user.UserService/GetUser
// {"id": "1"}

Performance Comparison

MetricRESTgRPC
Latency~100ms~14ms
Throughput100k req/s700k req/s
Message Size500KB70KB
ProtocolHTTP/1.1HTTP/2

FAQ

Q: Should I use gRPC for public APIs? A: gRPC is better for service-to-service. For public APIs, REST or GraphQL are more accessible since they use HTTP/1.1.

Q: Can I use gRPC with browsers? A: Yes, with gRPC-Web which runs over HTTP/1.1. gRPC natively requires HTTP/2.

Q: Is gRPC faster than REST? A: Yes, significantly. gRPC uses binary protocols and multiplexing.


gRPC is the modern choice for building high-performance microservices. REST remains better for public APIs, but for internal service communication, gRPC's speed and efficiency are unbeatable.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro