Build CRUD Server đơn giản với gRPC và node.js

gRPC là một framework RPC (remote procedure call) do Google phát triển, đã thu hút được nhiều sự quan tâm của cộng đồng software developer trong những năm vừa qua. Đặc biệt, gRPC được ứng dụng nhiều trong các hệ thống microservice bởi nhiều đặc tính vượt trội như: open source, không phụ thuộc vào ngôn ngữ lập trình, binary size nhỏ, hỗ trợ HTTP/2 và khả năng tương thích đa nền tảng. Định nghĩa của Google về gRPC như sau:

gRPC is a language-neutral, platform-neutral remote procedure call (RPC) framework and toolset developed at Google. It lets you define a service using Protocol Buffers, a particularly powerful binary serialization toolset and language. It then lets you generate idiomatic client and server stubs from your service definition in a variety of languages.

Tóm tắt thì gRPC server sẽ distribute các service method. Và gRPC client trên các device, trên các nền tảng khác nhau có thể call trực tiếp các service method này với cùng các parameter và kiểu dữ liệu trả về. gRPC sử dụng Protocol Buffer để truyền trực tiếp các byte dữ liệu cho các request và response message. So với việc RESTful API sử dụng JSON hoặc XML thì gRPC cho tốc độ xử lý nhanh hơn và kích thước message nhỏ gọn hơn. Protocol Buffer compiler sẽ sử dụng các file proto để convert các byte dữ liệu thành các kiểu dữ liệu tương ứng trên mỗi gRPC client. Chính vì ưu điểm này sẽ giúp giảm tải việc convert trên server, kích thước message nhỏ lại càng phù hợp với các ứng dụng mobile.

Trong bài viết này, chúng ta sẽ cùng nhau xây dựng một gRPC server cực kỳ đơn giản với node.js. Server này sẽ cung cấp các service đơn giản như create, read, update, delete cho object Pet (CRUD). Sau đó, chúng ta cũng sẽ tạo một gRPC client bằng node.js để call các service method trên từ gRPC server vừa tạo. Cụ thể:

  • Sử dụng npm và các package dependencies cần thiết để tạo gRPC server.
  • Tạo proto file để định nghĩa các gRPC message và service dùng cho Pet CRUD.
  • Tạo service method để lấy list các pet.
  • Tạo service method để tạo mới một object pet.
  • Tạo service method để xóa một object pet.
  • Tạo gRPC client sử dụng node.js.

Create npm project, add dependencies

Nếu máy tính của bạn chưa tải và cài đặt node.js, hãy làm theo hướng dẫn ở https://nodejs.org/en/download/

Để bắt đầu, hãy tạo mới một thư mục tên là gRPC-Server, chạy lệnh npm init để init một npm project.

Sau đó install 2 dependencies cần thiết bằng command line:

npm install --save grpc
npm install --save uuidv1
  1. grpc: dependency gRPC chính thức trong node.js
  2. uuid: dependency dùng để generate các unique UUID dựa trên timestamp.

Declare proto file

Tiếp theo tạo mới một file tên là pets.proto bằng lệnh touch pets.proto. Đây sẽ là file mà chúng ta define Protocol Buffer Message và các gRPC service. việc đầu tiên cần làm trong proto file đó là định nghĩa message Pet là ánh xạ của model object Pet. Pet model sẽ có 3 trường sau:

  1. id kiểu String
  2. name kiểu String
  3. description kiểu String

Ngoài ra, chúng ta cũng cần gắn một tag kiểu số cho mỗi field, set syntax thành version proto3:

// pets.proto
syntax = "proto3";

message Pet {
    string id = 1;
    string name = 2;
    string description = 3;
}

Implement and run gRPC server locally

Sau khi tạo được file proto, chúng ta sẽ implement gRPC server bằng node.js. Tạo file index.js chứa các main entry point của ứng dụng. Bên trong file này, chúng ta import grpc module, sau đó sử dụng gRPC load để load file pets.proto.

Tiếp theo, chúng ta khởi tạo object gRPC server và bind nó với localhost ở port 50051 rồi truyền vào object Server Credential cho mục đích test.

Cuối cùng là lệnh start gRPC server.

// index.js
const grpc = require('grpc')
const notesProto = grpc.load('pets.proto')
const server = new grpc.Server()
server.bind('127.0.0.1:50051', grpc.ServerCredentials.createInsecure())
console.log('Server running at http://127.0.0.1:50051')
server.start()

Thử chạy server bằng lệnh node index.js. Command line sẽ print ra message server running tương ứng.

Server running at http://127.0.0.1:50051
(node:3628) DeprecationWarning: grpc.load: Use the @grpc/proto-loader module with grpc.loadPackageDefinition instead
(Use `node --trace-deprecation ...` to show where the warning was created)

Create gRPC method to fetch list

Method đầu tiên mà chúng ta sẽ implement sẽ là method get list pet. Đầu tiên, chúng ta cần khai báo Empty message và PetList message như sau:

// pets.proto
syntax = "proto3";

message Pet {
    string id = 1;
    string name = 2;
    string description = 3;
}

message Empty {}

message PetList {
    repeated Pet pets = 1;
}

service PetService {
    rpc List (Empty) returns (PetList) {}
}

Empty message là một empty sub mặc định cho các request hoặc response mà không cần truyền vào/trả về parameter nào (tương tự kiểu Void trong một số ngôn ngữ lập trình). Còn đối với PetList message, chúng ta sử dụng keyword repeated để thể hiện kiểu dữ liệu dạng mảng Pet.

Tiếp theo, chúng ta khai báo PetService và RPC method List. Method này sẽ nhận param đầu vào là một Empty message và trả về kiểu PetList message. Sau đó trong file index.js, khởi tạo PetService bằng cách gọi hàm addService và truyền vào 2 param: instance của PetService và list param với callback trả về cho client list pet.

// index.js
const grpc = require('grpc')
const petsProto = grpc.load('pets.proto')
const pets = [
    { id: '1', name: 'Alaska', description: 'Description 1' },
    { id: '2', name: 'Husky', description: 'Description 2' }
]
const server = new grpc.Server()
server.addService(petsProto.PetService.service, {
    list: (_, callback) => {
        callback(null, pets)
    },
})
server.bind('127.0.0.1:50051', grpc.ServerCredentials.createInsecure())
console.log('Server running at http://127.0.0.1:50051')
server.start()

Để đơn giản cho việc lấy dữ liệu và test, trong bài viết này, chúng ta sẽ hard code một mảng các pet như trên, bên trong method list handler, trả về list pet này.

Restart lại server để deploy list pet service method vừa tạo.

Create gRPC client

Để call gRPC method và nhận response list pet, chúng ta cần phải tạo một gRPC node.js client. Tạo mới file client.js và import module gRPC. Sau đó trong file pets.proto và khởi tạo một client instance mới tương tư như khởi tạo gRPC server ở trên. Client sẽ kết nối đến localhost ở port 50051. Cuối cùng, chúng ta cần export client object vừa tạo:

// client.js
const grpc = require('grpc')
const PROTO_PATH = './pets.proto'
const PetService = grpc.load(PROTO_PATH).PetService
const client = new PetService('localhost:50051', grpc.credentials.createInsecure())
module.exports = client

Để gọi list pet method sử dụng gRPC client ở trên, tạo mới một file có tên là get_pets.js. Trong file này, chúng ta import client từ file client.js, sau đó gọi method list và truyền vào param đầu tiên một empty object. Param thứ 2 sẽ là callback handler với kết quả trả về với 2 variable error và response data. Ở đây, chúng ta sẽ kiểm tra error và print ra kết quả tương ứng:

// get_pets.js
const client = require('./client')
client.list({}, (error, pets) => {
    if (!error) {
        console.log('Fetch list pets successfully!')
        console.log(pets)
    } else {
        console.error(error)
    }
})

Để test thử gRPC client, run server gRPC trước sau đó chạy lệnh node get_pets.js, kết quả trả về sẽ được print ra trên command line như sau:

node get_pets.js
(node:1768) DeprecationWarning: grpc.load: Use the @grpc/proto-loader module with grpc.loadPackageDefinition instead
(Use `node --trace-deprecation ...` to show where the warning was created)
Fetch list pets successfully!
{
  pets: [
    { id: '1', name: 'Alaska', description: 'Description 1' },
    { id: '2', name: 'Husky', description: 'Description 2' }
  ]
}

Create gRPC method to insert new object

Method tiếp theo dùng để insert một object mới. Method này sẽ nhận request parameter là một Pet message và trả về object pet vừa insert vào response.

Trong file pets.proto, khai báo mới method Insert như sau:

// pets.proto
...
service PetService {
    rpc List (Empty) returns (PetList) {}
    rpc Insert (Pet) returns (Pet) {}
}

Trong file index.js, import module uuid/v1 ở đầu file, module này sẽ dùng để generate một unique uuid timstamp cho object được insert vào mảng pet. Bên trong function handler, chúng ta lấy ra object pet từ request, sau đó gán id mới object này bằng method uuidv1. Sau đó thêm object này vào mảng pet và trả về trong callback với error = null.

// index.js
...
server.addService(petsProto.PetService.service, {
    list: (_, callback) => {
        callback(null, pets)
    },
    insert: (call, callback) => {
        let pet = call.request
        pet.id = uuidv1()
        pets.push(pet)
        callback(null, pet)
    }
})
...

Để test method insert trên, tạo mới file insert_pet.js, tạo mới một object Pet, import client và call method insert. Sau đó print ra kết quả trả về.

// insert_pet.js

const client = require('./client')

let newPet = {
    name: "Shiba Inu",
    description: "Description 3"
}

client.insert(newPet, (error, pet) => {
    if (!error) {
       console.log('New pet created successfully', pet)
    } else {
       console.error(error)
    }
})

Restart server, execute lệnh note insert_pet.js được kết quả:

node insert_pet.js
(node:2776) DeprecationWarning: grpc.load: Use the @grpc/proto-loader module with grpc.loadPackageDefinition instead
(Use `node --trace-deprecation ...` to show where the warning was created)
New pet created successfully {
  id: 'c18055b0-5467-11eb-873d-ac402aad7f25',
  name: 'Shiba Inu',
  description: 'Description 3'
}

Create gRPC method to delete an object

Tương tự với insert method, chúng ta thêm khai báo cho delete method trong file proto. Tuy nhiên, cần khai báo thêm kiểu PetRequestId với một field id kiểu string, để làm parameter đầu vào cho delete method.

// pets.proto
...
message PetRequestId {
    string id = 1;
}

service PetService {
    rpc List (Empty) returns (PetList) {}
    rpc Insert (Pet) returns (Pet) {}
    rpc Delete (PetRequestId) returns (Pet) {}
}

Sửa implement trong index.js như sau:

server.addService(petsProto.PetService.service, {
    ...
    delete: (call, callback) => {
      let existingPetIndex = pets.findIndex(pet => pet.id === call.request.id)

      if (existingPetIndex != -1) {
          // Xóa object pet khỏi mảng pet
          pets.splice(existingPetIndex, 1)
          callback(null, {})
      } else {
          // Return error not found nếu không tồn tại pet có id tương ứng
          callback({
              code: grpc.status.NOT_FOUND,
              details: "Not found"
          })
      }
    }
})

Handle ở gRPC client với file delete_pet.js:

// delete_pet.js

const client = require('./client')

client.delete({ id: '1' }, (error, _) => {
    if (!error) {
        console.log('Pet has been deleted successfully!')
    } else {
      console.log('Delete pet failed!')
        console.error(error)
    }
})

Conclusion

Cuối cùng thì chúng ta cũng đã hoàn thiện và deploy thành công được một server gRPC cực kỳ đơn giản. Từ đó có thể distribute các remote precedure call tới các client đa nền tảng khác nhau. Ngoài các khả năng kể trên, gRPC còn support một số tính năng khác như authentication, bidirectional steaming, flow control, binding, cancelling và timeout... Vì vậy, còn rất nhiều thứ để khám phá và vọc vạch về gRPC.

Source article: https://www.alfianlosari.com/posts/building-grpc-server-note-crud-api-with-nodejs/

Final project: https://github.com/oNguyenXuanThanh/crud-grpc-nodejs


All Rights Reserved