I recently wanted to rewrite parts of an existing Node.js application in Rust. A complete rewrite would take a lot of time, so I was browsing through ways to rewrite some parts in Rust.

While it seems possible to call Rust from Node by compiling Rust into a Node js module using FFI, it felt a bit messy.

Instead I opted to look into gRPC, which I’ve been wanting to look into anyway. In this example, I have written a small function in both Node.js and Rust. It can be called using gRPC from either Node.js or Rust.

+--------------------+     +--------------------+
| Client (Rust/Node) | --> | Server (Rust/Node) |
+--------------------+     +--------------------+

res = mean(4, 5)       --> function mean(a, b)

As you can see, the client just wants to know the mean value of two numbers, and apparently needs the full power of another language to do that. As you can see, a totally reasonable example!

Parts

See https://github.com/zozs/grpc-rust-node for the GitHub repository with the complete code. Here I’ll just show some required parts.

I’ll show four parts, a client and server in Node.js, and a client and server in Rust. All four entities work together. You can for example have a server in Rust, which a Node.js client can connect to. Or the opposite way around. You choose :)

Protobuf

This is the Protobuf file with the gRPC service and messages. This describes the shared API between the server and client, and is used by both of the implementations. A nice thing is that Rust can use this to create strongly typed structs.

proto/rustnodegrpc.proto

syntax = "proto3";

package rustnodegrpc;

service Stats {
	rpc Mean (MeanRequest) returns (MeanResponse);
}

message MeanRequest {
	int32 a = 1;
	int32 b = 2;
}

message MeanResponse {
	double mean = 1;
}

As you can see, I define a single procedure Mean, with a request and response.

Rust client

src/client.rs

use rustnodegrpc::stats_client::StatsClient;
use rustnodegrpc::MeanRequest;

mod rustnodegrpc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = StatsClient::connect("http://127.0.0.1:9800").await?;
    let request = tonic::Request::new(MeanRequest { a: 5, b: 4 });

    let response = client.mean(request).await?.into_inner().mean;
    println!("mean: {}", response);

    Ok(())
}

Rust server

The server provides the implementation of the mean function, and launches a server process that listens for incoming connections.

src/server.rs

use tonic::{transport::Server, Request, Response, Status};

use rustnodegrpc::stats_server::{Stats, StatsServer};
use rustnodegrpc::{MeanRequest, MeanResponse};

mod rustnodegrpc;

#[derive(Default)]
pub struct MyStats {}

#[tonic::async_trait]
impl Stats for MyStats {
    async fn mean(
        &self,
        request: Request<MeanRequest>,
    ) -> Result<Response<MeanResponse>, Status> {
        let r = request.into_inner();
        println!("Got a request for: {:?}", &r);

        let reply = MeanResponse {
            mean: (r.a + r.b) as f64 / 2.0,
        };
        Ok(Response::new(reply))
    }
}


#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:9800".parse().unwrap();
    let server = MyStats::default();

    println!("StatsServer listening on {}", addr);

    Server::builder()
        .add_service(StatsServer::new(server))
        .serve(addr)
        .await?;

    Ok(())
}

Node.js client

Now a client in Node.js too!

node/client.js

import * as grpc from '@grpc/grpc-js'
import * as protoLoader from '@grpc/proto-loader'
import { promisify } from 'util'

const packageDefinition = protoLoader.loadSync('../proto/rustnodegrpc.proto')
const { rustnodegrpc } = grpc.loadPackageDefinition(packageDefinition)

// Create client
const client = new rustnodegrpc.Stats('127.0.0.1:9800', grpc.credentials.createInsecure())

// Get mean by talking to server, using a callback
client.mean({ a: 4, b: 5 }, (err, response) => {
  console.log('response:', response)
})

// Get mean by talking to sereither by callback or as promisever, using a promise
const meanFunc = promisify(client.mean).bind(client)

console.log('response:', await meanFunc({ a: 5, b: 4 }))

Node.js server

The server provides the implementation of the mean function, and launches a server process that listens for incoming connections.

node/server.js

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

const packageDefinition = protoLoader.loadSync('../proto/rustnodegrpc.proto')
const { rustnodegrpc } = grpc.loadPackageDefinition(packageDefinition)

// Implemented function
function mean(call, callback) {
  const { a, b } = call.request
  console.log('got request:', call.request)
  callback(null, { mean: (a + b) / 2 })
}

// Launch service
const server = new grpc.Server()

// load service Stats from proto file
server.addService(rustnodegrpc.Stats.service, { mean })
server.bindAsync('127.0.0.1:9800', grpc.ServerCredentials.createInsecure(), () => {
  server.start()
})

Running the example

First we prepare the server. It needs to be running before the client starts.

$ cd node/
$ npm i
$ node server.js

OR FOR RUST
$ cargo run --bin server

Then we build and run the client in a new shell.

$ cargo run --bin client
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/client`
mean: 4.5

OR FOR NODE JS

$ cd node/
$ npm i
$ node client.js
response: { mean: 4.5 }

You should see the client printing out the correct mean (4.5) of the numbers 4 and 5. Success!

While I haven’t used this in any big project yet, I think this seems to be a super nice way to mix different languages within a single application. There are a lot of language bindings for gRPC. It should also make it possible for me to partially rewrite some of my existing apps to use more Rust.