ساخت API  با استفاده از gRPC در Node js

در حال گشت و گذار تو ویرگول بودم که مقاله جالبی از عرفان توجهم رو جلب کرد. تو این مقاله در مورد gRPC و تفاوت اون با REST توضیح داده بود. توضیحات قبل و بعدش هم کامل بود و اینجا هر زمانی که نیاز بشه بهش استناد می‌کنم. بعد خوندنش تصمیم گرفتم gRPC رو روی Node js هم پیاده سازی کنم و ساختار API پایه بنویسم. http://vrgl.ir/Le84W ‏gRPC چیه؟‏ gRPC یک فریم ورک که گوگل اون رو توسعه داده و مزیت اصلیش امنیت و سرعت فوق العادش نسبت به معماری REST هستش. این مزیت ها به خاطر دلایل پایینه:با استاندارد HTTP/2 ساخته شده. تو این استاندارد دیگ برای هر درخواست یک tcp جداگونه باز نمیشه و برای انتقال دیتا از باینری استفاده می‌کنه که نسبت به استاندارد HTTP/1 خیلی سریع‌تر و چابک‌تره. برای اینکه بیشتر در مورد HTTP/2 بدونید اینجا رو بخونید.از پروتکل بافر استفاده می‌کنه. پروتکل بافر در واقع یک ساختار دیتا هستش که از قواعد خاصی پیروی میکنه.دیتا رو کمپرس می‌کنه و تقریبا ۷ برابر از json سریعتره. اطلاعات کاملتر رو اینجا بخونید.از message به جای verb ها در REST استفاده می‌کنه. وقتی که تو معماری REST توسعه می‌دیم عملاً متدها یا GET هستن یا POST . (متدهای PUT,PATCH, DELETE هم به نوعی POST محسوب میشن) این امکان دست توسعه دهنده رو باز میزاره تا متدهای دلخواه خودش رو هم بدون محدودیت ایجاد کنه.برای مقایسه کامل‌تر REST و gRPC می‌تونید این مقاله رو بخونید.راه اندازی پروژهیک دایرکتوری جدید برای پروژه می‌سازیم.$ mkdir NODE-GRPC $ cd NODE-GRPCو این ساختار رو ایجاد می‌کنیم.├── client │ ├── app.js │ └── package.json ├── protos └── server ├── index.js └── package.jsonمن برای این پروژه از PostgreSQL استفاده کردم ، ولی شما می‌تونید از هر دیتابیسی استفاده کنید.یک دیتابیس به نام grpc_products می‌سازیم.سرویس های gRPCکاری که امروز میخوایم انجام بدیم ساخت API با استفاده از gRPC برای انجام عملیات CRUD هستش.در gRPC مشابه Socket ما یک سرور و یک کلاینت داریم. اول از همه سرور رو شروع می‌کنیم. تو فولدر protos یک فایل با عنوان product.proto می‌سازیم که تو اون سرویس‌ها و مسیج‌های gRPC رو می‌نویسیم.syntax = "proto3"; package product; // service definition service ProductService { rpc listProducts(Empty) returns (ProductList) {} rpc readProduct(ProductId) returns (Product) {} rpc createProduct(newProduct) returns (result) {} rpc updateProduct(Product) returns (result) {} rpc deleteProduct(ProductId) returns (result) {} } // message type definitions message Empty {} message ProductList { repeated Product products = 1; } message ProductId { int32 id = 1; } message Product { int32 id = 1; string name = 2; string price = 3; } message newProduct { string name = 1; string price = 2; } message result { string status = 1; }تو این قسمت اول ورژن پروتوبافر رو مشخص کردیم و بعد هم براش یک package ساختیم. ساخت پکیج برای فایل پروتو الزامی نیست ولی ایده خوبیه تا تو پروژه های بزرگ گیج نشیم.بعد از اون متدهای gRPC رو که معادل عملیات پایه ای CRUD هست رو نوشتیم.CRUD gRPC methodIndex listProductsStore createProductUpdate updateProductGet readProductDelete deleteProduct متد listProducts یک مسیج Empty رو به عنوان ورودی دریافت و ProductList رو برمی‌گردونه.حالا فایل `package.json` اپدیت می‌کنیم.{ "name": "node-grpc-server", "dependencies": { "@grpc/proto-loader": "^0.4.0", "google-protobuf": "^3.6.1", "grpc": "^1.18.0", "knex": "^0.16.3", "pg": "^7.8.0" }, "scripts": { "start": "node index.js" } }کاربرد هرکدوم از این ماژول ها تو شکل پایینه.@grpc/proto-loader: loads .proto files google-protobuf: JavaScript version of the Protocol Buffers runtime library grpc: Node gRPC Library knex: SQL query builder for Node pg: PostgreSQL client for Nodeقبل از نصب پکیج ها باید protoc رو روی ماشین‌مون نصب کرده باشیم.از این لینک می‌تونید استفاده کنید.بعد نصب protoc باید پیکج های سرور رو نصب کنیم.$ cd server $ npm installبعد فایل knexfile.js رو در فولدر server که برای ست کردن کانفیگ دیتابیس و میگریشن ها هست رو ایجاد می‌کنیم.const path = require('path'); module.exports = { development: { client: 'postgresql', connection: { host: '127.0.0.1', user: '', password: '', port: '5432', database: 'grpc_products', }, pool: { min: 2, max: 10, }, migrations: { directory: path.join(__dirname, 'db', 'migrations'), }, seeds: { directory: path.join(__dirname, 'db', 'seeds'), }, }, };بعد از وارد کردن اطلاعات دیتابیس باید فولدری برای میگرشن ها و سیدرها بسازیم. ساختار پروژه بعد ساخت این فولدرها به شکل زیره.├── client │ ├── app.js │ └── package.json ├── protos │ └── product.proto └── server ├── db │ ├── migrations │ └── seeds ├── index.js ├── knexfile.js ├── package-lock.json └── package.jsonیک میگرشن جدید می‌سازیم.$ ./node_modules/.bin/knex migrate:make productsو کدهای زیر رو داخلش قرار می‌دیم.exports.up = function (knex, Promise) { return knex.schema.createTable('products', function (table) { table.increments(); table.string('name').notNullable(); table.string('price').notNullable(); }); }; exports.down = function (knex, Promise) { return knex.schema.dropTable('products'); };حالا یک سیدر می‌سازیم.$ ./node_modules/.bin/knex seed:make productو تعدای دیتای فیک می‌سازیم.exports.seed = function (knex, Promise) { // Deletes ALL existing entries return knex('products').del() .then(function () { // Inserts seed entries return knex('products').insert([ { name: 'pencil', price: '100' }, { name: 'pen', price: '550' }, { name: 'book', price: '98' }, ]); }); };و سیدر رو ران می‌کنیم تا دیتابیس پر شه.$ ./node_modules/.bin/knex seed:runحالا فایل index.js رو آپدیت می‌کنیم.// requirements const path = require('path'); const protoLoader = require('@grpc/proto-loader'); const grpc = require('grpc'); // knex const environment = process.env.ENVIRONMENT || 'development'; const config = require('./knexfile.js')[environment]; const knex = require('knex')(config); // grpc service definition const productProtoPath = path.join(__dirname, '..', 'protos', 'product.proto'); const productProtoDefinition = protoLoader.loadSync(productProtoPath); const productPackageDefinition = grpc.loadPackageDefinition(productProtoDefinition).product; /* Using an older version of gRPC? (1) You won't need the @grpc/proto-loader package (2) const productPackageDefinition = grpc.load(productProtoPath).product; */ // knex queries function listProducts(call, callback) {} function readProduct(call, callback) {} function createProduct(call, callback) {} function updateProduct(call, callback) {} function deleteProduct(call, callback) {} // main function main() { const server = new grpc.Server(); // gRPC service server.addService(productPackageDefinition.ProductService.service, { listProducts: listProducts, readProduct: readProduct, createProduct: createProduct, updateProduct: updateProduct, deleteProduct: deleteProduct, }); // gRPC server server.bind('localhost:50051', grpc.ServerCredentials.createInsecure()); server.start(); console.log('gRPC server running at http://127.0.0.1:50051'); } main();برای تبدیل فایل پروتو به js از پیکج زیر استفاده می‌کنیم.$ npm install -g grpc-toolsبعد از نصب پکیج تو روت پروژه کامند زیر رو ران می‌کنیم تا پروتو به js تبدیل شه.$ protoc -I=. ./protos/product.proto \ --js_out=import_style=commonjs,binary:./server \ --grpc_out=./server \ --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin`بعد ران شدن کامند دو تا فایل در پوشه server/protos ساخته میشه. فعلا کار ما با سرور تمومه می‌تونیم با دستور npm start ن رو ران کنیم و اگر مشکلی نباشه باید پیام زیر رو ببینید.gRPC server running at http://127.0.0.1:50051حالا قبل از اینکه کویری بزنیم و اطلاعات رو فتچ کنیم بیاید کلاینت رو بسازیم.مثل سرور از فایل package.json شروع می‌کنیم.{ "name": "node-grpc-client", "dependencies": { "@grpc/proto-loader": "^0.4.0", "body-parser": "^1.18.3", "express": "^4.16.4", "google-protobuf": "^3.6.1", "grpc": "^1.18.0" }, "scripts": { "start": "node app.js" } }پکیج ها رو نصب می‌کنیم و فایل app.js و آپدیت می‌کنیم.// requirements const express = require('express'); const bodyParser = require('body-parser'); const productRoutes = require('./routes/productRoutes'); // express const app = express(); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); // routes app.use('/api', productRoutes); // run server app.listen(3000, () => { console.log('Server listing on port 3000'); });در اینجا یک سرور با اکسپرس به همراه روت‌ها تعریف کردیم.هم چنین در فایل client/routes/grpcRoutes.js کدهای زیر رو اضافه می‌کنیم.// requirements const path = require('path'); const protoLoader = require('@grpc/proto-loader'); const grpc = require('grpc'); // gRPC client const productProtoPath = path.join(__dirname, '..', '..', 'protos', 'product.proto'); const productProtoDefinition = protoLoader.loadSync(productProtoPath); const productPackageDefinition = grpc.loadPackageDefinition(productProtoDefinition).product; const client = new productPackageDefinition.ProductService( 'localhost:50051', grpc.credentials.createInsecure()); /* Using an older version of gRPC? (1) You won't need the @grpc/proto-loader package (2) const productPackageDefinition = grpc.load(productProtoPath).product; (3) const client = new productPackageDefinition.ProductService( 'localhost:50051', grpc.credentials.createInsecure()); */ // handlers const listProducts = (req, res) => {}; const readProduct = (req, res) => {}; const createProduct = (req, res) => {}; const updateProduct = (req, res) => {}; const deleteProduct = (req, res) => {}; module.exports = { listProducts, readProduct, createProduct, updateProduct, deleteProduct, };حالا با ران کردن کامند npm start باید پیام زیر رو ببینیم.Server listing on port 3000بعد از هر تغییری سرور و کلاینت رو ریستارت کنید.حاال به سرور برگردیم و مثلا عملیات خوندن لیست کامل همه محصولات رو بنویسیم.Server : function listProducts(call, callback) { /* Using 'grpc.load'? Send back an array: 'callback(null, { data });' */ knex('products') .then((data) => { callback(null, { products: data }); }); }Client : const listProducts = (req, res) => { /* gRPC method for reference: listProducts(Empty) returns (ProductList) */ client.listProducts({}, (err, result) => { res.json(result); }); };حالا اگر این اندپوینت رو تست کنیم باید دیتای محصولات که با سیدر ران کرده بویم ببینیم.$ curl http://127.0.0.1:3000/api/productsخروجی باید به صورت زیر باشه.{ "products": [ { "id": 1, "name": "pencil", "price": "100" }, { "id": 2, "name": "pen", "price": "550" }, { "id": 3, "name": "book", "price": "98" } ] }بقیه اندپوینت‌ها هم مشابه این ساخته میشه و میشه هر کنترلر دلخواهی روش نوشت.اگر مشتاقید مقاله کامل این موضوع رو بخونید میتونید از لینک زیر پیداش کنید. https://mherman.org/blog/node-grpc-postgres/