Table of Contents
In this post, we'll build a simple to-do app using the gRPC protocol, following these steps:
Initialize the Project
1. Start a New Node.js Project
Create a new directory for your project:
$ mkdir todo-grpc
Navigate to the newly created directory and initialize the Node.js project:
$ npm init
This command generates a package.json
file. In this file, add the following entry to enable ES6 modules instead of CommonJS:
"type": "module"
2. Install Dependencies
Install the necessary dependencies:
@grpc/grpc-js
: The gRPC client@grpc/proto-loader
: A utility for loading.proto
files for gRPC services
$ npm install @grpc/grpc-js @grpc/proto-loader
Here are the exact versions used in this post:
"@grpc/grpc-js": "^1.12.6",
"@grpc/proto-loader": "^0.7.13"
3. Create Required Files
Create the required files for the project:
$ touch todo.proto server.js client.js
Defining the Schema
Let’s start by defining the schema for communication between the server and the client.
In the todo.proto
file, create a package called todoPackage
and define a service named Todo
. This service will expose methods for communication:
createTodo
: Creates a new to-do item. It accepts aTodoItem
(which we'll define later) and returns the createdTodoItem
.readTodos
: Retrieves all to-dos from the server. It takes no parameters and returns a list ofTodoItems
.readTodosStream
: Streams to-dos from the server one by one. This method is more efficient for handling large datasets compared toreadTodos
, which returns all to-dos in one go.
syntax = "proto3";
package todoPackage;
service Todo {
// Unary gRPC
rpc createTodo(TodoItem) returns(TodoItem);
// Synchronous
rpc readTodos(NoParams) returns(TodoItems);
// Server streaming
rpc readTodosStream(NoParams) returns(stream TodoItem);
};
We also need to define the types used in the method parameters and return types:
NoParams
: A method with no parameters.TodoItem
: Defines the structure of a single to-do item.TodoItems
: Contains a propertyitems
, which is a list ofTodoItem
objects. Lists/arrays are represented using therepeated
keyword.
message NoParams {};
message TodoItem {
int32 id = 1;
string text = 2;
};
message TodoItems {
repeated TodoItem items = 1;
};
Building the Server
Now, let’s build the server.
First, import the necessary dependencies:
import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";
Next, load the schema we defined in todo.proto
:
const packageDef = protoLoader.loadSync("todo.proto", {});
const grpcObject = grpc.loadPackageDefinition(packageDef);
// Access the defined package using dot notation
const todoPackage = grpcObject.todoPackage;
Create an instance of the gRPC server and add the Todo
service:
const server = new grpc.Server();
server.addService(todoPackage.Todo.service, {
createTodo: createTodo,
readTodos: readTodos,
readTodosStream: readTodosStream,
});
const todos = [];
Next, implement the methods for the Todo
service:
// Unary gRPC method
function createTodo(call, callback) {
const todoItem = {
id: todos.length,
text: call.request.text,
};
todos.push(todoItem);
callback(null, todoItem);
}
// Sync gRPC method
function readTodos(call, callback) {
callback(null, { items: todos });
}
// Server streaming gRPC method
function readTodosStream(call) {
todos.forEach((todo) => {
call.write(todo);
});
call.end();
}
The todos
array acts as in-memory storage for the to-dos.
Key Notes:
-
createTodo
method:- We access the incoming to-do item via
call.request
, which is similar to accessingrequest.body
in REST APIs. - After saving the to-do in the array, we call the callback with the created item.
- We access the incoming to-do item via
-
readTodos
method:- This method returns all to-dos at once by invoking the callback with the full list.
-
readTodosStream
method:- We stream each to-do item individually to the client.
Finally, bind the server to a port and start listening:
server.bindAsync(
"0.0.0.0:3000",
grpc.ServerCredentials.createInsecure(),
() => {
console.log("Server is listening on port 3000");
}
);
Building the Client
The client setup is similar to the server, so we will skip the repeated steps.
import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";
const packageDef = protoLoader.loadSync("todo.proto", {});
const grpcObject = grpc.loadPackageDefinition(packageDef);
const todoPackage = grpcObject.todoPackage;
Next, create a new instance of the Todo
service and specify the server's address and port:
const client = new todoPackage.Todo(
"0.0.0.0:3000",
grpc.credentials.createInsecure()
);
To run the client, execute the following command, passing the to-do text as an argument:
node client.js "Buy some books"
Inside client.js
, access the command-line argument:
const text = process.argv[2];
Then, invoke the gRPC methods:
// Unary gRPC method
client.createTodo({ id: -1, text }, (err, res) => {
console.log({
create: res,
});
});
// Sync gRPC method
client.readTodos({}, (err, res) => {
res.items.forEach((item) => {
console.log(item.text);
});
});
// Server streaming method
const call = client.readTodosStream();
call.on("data", (item) => {
console.log(item.text);
});
call.on("end", () => console.log("Server done streaming"));
Explanation of Calls:
- The first call creates a new to-do on the server.
- The second call retrieves all to-dos from the server.
- The third call streams to-dos from the server, with event listeners handling incoming data and the end of the stream.
Running the Project
- In one terminal, start the server:
node server.js
- In another terminal, run the client multiple times with different to-do texts:
node client.js "Buy some books"
That’s it! Thanks for reading!