Creating a CRUD gRPC Service with MongoDB using Node.js: A Comprehensive Guide

39 views

To create a gRPC service with CRUD operations backed by MongoDB in Node.js, you'll need to set up the gRPC service, define the Protobuf schema for the operations, and implement the CRUD operations using the MongoDB client.

Below is a comprehensive guide to setting up a basic CRUD gRPC service using Node.js and MongoDB.

Prerequisites

  1. Node.js: Download and install Node.js from nodejs.org.
  2. MongoDB: Set up a MongoDB instance. You can use a local instance or MongoDB Atlas for a cloud-hosted MongoDB.
  3. Protobuf: Download and install the Protocol Buffers compiler (protoc) from the Protocol Buffers GitHub page.

Step-by-Step Guide

  1. Define the Service Using Protobuf

    Create a file named crud.proto with the following content:

    syntax = "proto3";
    
    package example;
    
    service CrudService {
      rpc Create (CreateRequest) returns (CreateResponse);
      rpc Read (ReadRequest) returns (ReadResponse);
      rpc Update (UpdateRequest) returns (UpdateResponse);
      rpc Delete (DeleteRequest) returns (DeleteResponse);
    }
    
    message CreateRequest {
      string name = 1;
      string data = 2;
    }
    
    message CreateResponse {
      string id = 1;
    }
    
    message ReadRequest {
      string id = 1;
    }
    
    message ReadResponse {
      string id = 1;
      string name = 2;
      string data = 3;
    }
    
    message UpdateRequest {
      string id = 1;
      string name = 2;
      string data = 3;
    }
    
    message UpdateResponse {
      bool success = 1;
    }
    
    message DeleteRequest {
      string id = 1;
    }
    
    message DeleteResponse {
      bool success = 1;
    }
    
  2. Generate gRPC Code

    Use the protoc compiler to generate the service code for Node.js:

    mkdir -p out
    protoc --js_out=import_style=commonjs,binary:out --grpc_out=out --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` crud.proto
    

    This command will generate out/crud_pb.js and out/crud_grpc_pb.js.

  3. Setup Project

    Create a new Node.js project and install the required dependencies:

    mkdir grpc_mongo_example
    cd grpc_mongo_example
    npm init -y
    npm install @grpc/grpc-js @grpc/proto-loader mongodb
    
  4. Create Server

    Create a file named server.js:

    const grpc = require("@grpc/grpc-js");
    const protoLoader = require("@grpc/proto-loader");
    const { MongoClient, ObjectId } = require("mongodb");
    
    const PROTO_PATH = "./crud.proto";
    
    // Load protobuf
    const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
      keepCase: true,
      longs: String,
      enums: String,
      defaults: true,
      oneofs: true,
    });
    const crudProto = grpc.loadPackageDefinition(packageDefinition).example;
    
    // MongoDB configuration
    const url = "mongodb://localhost:27017";
    const dbName = "grpcdb";
    const client = new MongoClient(url, { useNewUrlParser: true, useUnifiedTopology: true });
    
    // Implement CRUD operations
    const create = async (call, callback) => {
      try {
        const db = client.db(dbName);
        const { name, data } = call.request;
        const result = await db.collection("items").insertOne({ name, data });
        callback(null, { id: result.insertedId.toString() });
      } catch (err) {
        callback(err, null);
      }
    };
    
    const read = async (call, callback) => {
      try {
        const db = client.db(dbName);
        const { id } = call.request;
        const item = await db.collection("items").findOne({ _id: new ObjectId(id) });
        if (!item) {
          callback(new Error("Item not found"), null);
        } else {
          callback(null, { id: item._id.toString(), name: item.name, data: item.data });
        }
      } catch (err) {
        callback(err, null);
      }
    };
    
    const update = async (call, callback) => {
      try {
        const db = client.db(dbName);
        const { id, name, data } = call.request;
        const result = await db.collection("items").updateOne({ _id: new ObjectId(id) }, { $set: { name, data } });
        if (result.matchedCount === 0) {
          callback(new Error("Item not found"), null);
        } else {
          callback(null, { success: result.matchedCount > 0 });
        }
      } catch (err) {
        callback(err, null);
      }
    };
    
    const deleteItem = async (call, callback) => {
      try {
        const db = client.db(dbName);
        const { id } = call.request;
        const result = await db.collection("items").deleteOne({ _id: new ObjectId(id) });
        callback(null, { success: result.deletedCount > 0 });
      } catch (err) {
        callback(err, null);
      }
    };
    
    // Start the server
    const main = async () => {
      await client.connect();
      const server = new grpc.Server();
      server.addService(crudProto.CrudService.service, { Create: create, Read: read, Update: update, Delete: deleteItem });
      server.bindAsync("127.0.0.1:50051", grpc.ServerCredentials.createInsecure(), () => {
        server.start();
        console.log("Server running at http://127.0.0.1:50051");
      });
    };
    
    main();
    
  5. Create Client

    Create a file named client.js:

    const grpc = require("@grpc/grpc-js");
    const protoLoader = require("@grpc/proto-loader");
    
    const PROTO_PATH = "./crud.proto";
    
    // Load protobuf
    const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
      keepCase: true,
      longs: String,
      enums: String,
      defaults: true,
      oneofs: true,
    });
    const crudProto = grpc.loadPackageDefinition(packageDefinition).example;
    
    const client = new crudProto.CrudService("localhost:50051", grpc.credentials.createInsecure());
    
    // Utility function to print item
    function printItem(err, response) {
      if (err) {
        console.error(err);
      } else {
        console.log(response);
      }
    }
    
    // Create a new item
    client.Create({ name: "Test Item", data: "Some data" }, (err, response) => {
      printItem(err, response);
    
      // Read the item
      client.Read({ id: response.id }, (err, response) => {
        printItem(err, response);
    
        // Update the item
        client.Update({ id: response.id, name: "Updated Item", data: "Updated data" }, (err, response) => {
          printItem(err, response);
    
          // Delete the item
          client.Delete({ id: response.id }, (err, response) => {
            printItem(err, response);
          });
        });
      });
    });
    
  6. Run the Server and Client

    First, start the server:

    node server.js
    

    Then, in another terminal, run the client:

    node client.js
    

    The client will perform the following operations in sequence: create a new item, read the item, update the item, and delete the item, printing the results of each operation to the console.

Conclusion

This example demonstrates how to create a simple CRUD gRPC service backed by MongoDB using Node.js. By defining the service with Protocol Buffers and utilizing gRPC and MongoDB libraries in Node.js, you can build efficient and scalable services that perform CRUD operations. This setup also highlights Node.js’s capability to interoperate with various databases and protocols, making it a versatile choice for backend development.