Chidume Nnamdi I'm a software engineer with over six years of experience. I've worked with different stacks, including WAMP, MERN, and MEAN. My language of choice is JavaScript; frameworks are Angular and Node.js.

How to build a gRPC server in Dart

11 min read 3253

How to Build a gRPC Server in Dart

In this tutorial, we’ll cover the basics of gRPC, a performant, open-source, universal RPC framework, review a little about the Dart programming language, and demonstrate how to build a gRPC server in Dart.

We’ll walk you through the following:

What is gRPC?

gRPC is an interprocess communication (RPC) framework built by Google and released in 2015. It is open-source, language-neutral, and has a compact binary size. gRPC also supports HTTP/2 and is cross-platform compatible.

gRPC is very different from the conventional RPC in the sense that it uses Protocol Buffers as its IDL to define its service interface. Protocol buffers is a serialization tool built by Google that allows you to define your data structures, then use the protocol buffer compiler to generate source code from these data structures to the language of your choice. The generated language is used to write and read the data structures to and from any context we want. According to the official docs, “Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler.”

The protocol buffer is used to write the service definition interface, which is used to define data structures and methods. The data structures are like data types in statically typed languages such as Java; they tell the compiler/interpreter how the data is intended to be used. The data structures in the service definition interface are the argument types that will be passed to the methods and the return types of the methods. This service definition interface is kept in a text file with .proto extension. The methods in the service interface are the methods the gRPC server will expose to be called by gRPC clients.

gRPC has three components:

  1. server hosts the methods implementation and listens for requests from clients
  2. protocol buffer holds the message format of the data structures and the methods, including their arguments and return type
  3. client calls the methods hosted by the server. The client knows about the methods and their return and argument types from the service definition interface in the proto file

Using this service interface, the gRPC server sets up its server code implementing the methods in the service interface. It sets itself up and listens for requests (method calls) from clients.

The client uses the service definition interface to generate the client stub. This client sub is from where the methods in the server are called. A gRPC client app can make direct requests to a server application. Both client and server embrace a common interface, like a contract, in which it determines what methods, types, and returns each of the operations is going to have.

How protocol buffers work

The most appealing thing about gRPC is its use of the protocol buffer, which enables the protocol to be platform-agnostics and polyglot. That means the server can be written in a given language and the client developed in another language. The protocol buffer makes this possible because it has compilers that can generate a language source code from the data structure in its definitions.

For example, let’s say the server is to be written in JavaScript. We’ll use the proto compiler to generate JavaScript source code from the definitions in the .proto file. The server can then access and manipulate the data structures and methods using JavaScript code.

We made a custom demo for .
No really. Click here to check it out.

For the client, we want it to be developed in Java, so we’ll generate Java source code from the definitions. The client can then call the methods and access the data structures using Java code. That’s what we mean when we say that gRPC is polyglot and platform-agnostic.

Note that protocol buffers are not only used by gRPC. They can also be used for serialization. It is commonly used to send data through streams so you can read and write your data structure without any overhead loss.

Building a gRPC server in Dart

Now that we understand the basics of gRPC and protocol buffers, it’s time to build our gRPC server in Dart.

Before we begin, make sure you have the Dart SDK installed in your machine. The Dart executable must be available globally in your system. Run the following command to check:

➜  grpc-dart dart --version
Dart SDK version: 2.10.5 (stable) (Tue Jan 19 13:05:37 2021 +0100) on "macos_x64"

We’ll also need some protoc tools. Since we’re developing the gRPC server in Dart, we’ll have to install the proto compiler for the Dart lang. This compiler will generate Dart source code from the service definitions in the .proto file.

The protocol buffer compiler is a command-line tool for compiling the IDL code in the .proto files and generating specified language source code for it. For installation instructions, see the gRPC docs. Make sure to download the version 3.

Finally, the Dart plugin for the protoc compiler generates the Dart source code from the IDL code in the .proto files.

For Mac users, install the Dart protoc plugin by running the following command:

dart pub global activate protoc_plugin

This installs the protoc_plugin globally in your machine.

Next, update the $PATH so the protoc will see our plugin:

export PATH="$PATH:$HOME/.pub-cache/bin"

Now it’s time to create the server.
For our demonstration, we’ll create a gRPC server that manages a book service. This service will expose methods that will be used to:

  • Get all books (GetAllBooks)
  • Get a book from the server via its ID (GetBook)
  • Delete a book (DeleteBook)
  • Edit a book (EditBook)
  • Create a book (CreateBook)

Our Dart project will be a console-simple project. Run the following command to scaffold the Dart project:

dart create --template=console-simple dart_grpc

The create subcommand tells the Dart executable that we wish to create a Dart project. --template=console-simple tells the Dart exe that we want the Dart project to be a simple console application.

The output will be as follows:

Creating /Users/.../dart_grpc using template console-simple...

  .gitignore
  CHANGELOG.md
  README.md
  analysis_options.yaml
  bin/dart_grpc.dart
  pubspec.yaml

Running pub get...                     10.2s
  Resolving dependencies...
  Downloading pedantic 1.9.2...
  Downloading meta 1.2.4...
  Changed 2 dependencies!

Created project dart_grpc! In order to get started, type:

  cd dart_grpc

➜

Our project will reside in the dart_grpc folder.

Open the pubspec.yaml file. This is where we set the configs and dependencies on a Dart application. We want to install the grpc and protobuf dependencies. Add the below line in the pubspec.yaml file and save:

dependencies:
  grpc:
  protobuf:

Now, run pub get in your console so the dependencies are installed.

Writing service definitions

We define our service definitions in a .proto file. So let’s create a book.proto file.

touch book.proto

Add the below Protobuf code in the book.proto file:

syntax = "proto3";

service BookMethods {
    rpc CreateBook(Book) returns (Book);
    rpc GetAllBooks(Empty) returns (Books);
    rpc GetBook(BookId) returns (Book);
    rpc DeleteBook(BookId) returns (Empty) {};
    rpc EditBook(Book) returns (Book) {};
}

message Empty {}

message BookId {
    int32 id = 1;
}

message Book {
    int32 id = 1;
    string title = 2;
}

message Books {
    repeated Book books = 1;
}

That’s a lot of code. Let’s go through it line by line.

syntax = "proto3";

Here, we are telling the protocol buffer compiler that we’ll be using version 3 of the protocol buffer lang.

service BookMethods {
    rpc CreateBook(Book) returns (Book);
    rpc GetAllBooks(Empty) returns (Books);
    rpc GetBook(BookId) returns (Book);
    rpc DeleteBook(BookId) returns (Empty) {};
    rpc EditBook(Book) returns (Book) {};
}

Here, we’re declaring the methods and the service they will be under. The service keyword denotes a single service in a gRPC, so we create a service BookMethods. To call a method, the method must be referenced by its service. This is analogous to class and methods; methods are called through their class instance. We can have several services defined in a proto.

Methods are denoted inside each service by the rpc keyword. The rpc tells the compiler that the method is an rpc endpoint and will be exposed and called from clients remotely. In our definition, we have five methods inside the BookMethods service: CreateBook, GetAllBooks, GetBook, DeleteBook, and EditBook.

  • CreateBook takes a Book data type as arg and returns a Book type. This method implementation will create a new book
  • GetAllBooks takes an Empty type as arg and returns a Books type. Its implementation will return all the books
  • GetBook method accepts an input param of type, BookId and returns a Book. Its implementation will return a specific book
  • DeleteBook takes a BookId type as input param and returns an Empty type. Its implementation will delete a book entry from the collection
  • EditBook takes a Book type as arg and returns a Book type. Its implementation will modify a book in the collection

All the other data from this point down represents the data or message types. We have:

message Empty {}

The message keyword denotes message types. Each message type has fields and each field has a number to uniquely identify it in the message type.

Empty denotes an empty data structure. This is used when we want to send no argument to rpc methods or when the methods return no value. It is the same as void in C/C++.

message BookId {
    int32 id = 1;
}

This data structure represents a book ID message object. The id field will hold an integer going by the int32 keyword before it. The id field will hold the ID of a book.

message Book {
    int32 id = 1;
    string title = 2;
}

This data structure represents a book. The id field holds the book’s unique ID and the title holds the book’s title. The title field will be a string going by the string keyword before it.

message Books {
    repeated Book books = 1;
}

This represents an array of books. The books field is an array that holds books. repeated denotes a field that will be a list or an array. The Book before the field name denotes that the array will be of Book types.

Now that we’re done writing our service definition, let’s compile the book.proto file.

Compiling proto

The protoc tool is used to compile our .proto files. Make sure the protoc tool is globally available in your system:

protoc --version
libprotoc 3.15.8

That’s the version of my protoc tool at the time of this writing. your version might be different, it does not matter.

Now, make sure your terminal is opened at the dart_grpc root folder. Run the below command to compile the book.proto file:

protoc -I=. --dart_out=grpc:. book.proto

The I=. tells the compiler the source folder which proto field we are trying to compile.

The dart_out=grpc:. subcommand tells the protoc compiler that we’re generating Dart source code from the book.proto definitions and using it for gRPC =grpc:. The . tells the compiler to write the dart files in the root folder we are operating from.

This command will generate the following files:

  • book.pb.dart
  • book.pbenum.dart
  • book.pbgrpc.dart
  • book.pbjson.dart

The most important file is book.pb.dart, which contains Dart source code for the message data structures in the book.proto file. It also contains Dart classes for Empty, BookId, Book, and Books. From these, we create their instances and use them when calling the rpc methods.

The book.grpc.dart file contains the class BookMethodClient, which we’ll use to create instances to call the rpc methods and an interface BookMethodsServiceBase. This interface will be implemented by the server to add the methods’ implementations.

Next, we’ll write our server code.

Creating the gRPC server

We’ll write our gRPC server code in the dart_grpc.dart file. Open the file and paste the below code:

import 'package:grpc/grpc.dart';
import 'package:grpc/src/server/call.dart';
import './../book.pb.dart';
import './../book.pbgrpc.dart';

class BookMethodsService extends BookMethodsServiceBase {
  Books books = Books();

  @override
  Future<Book> createBook(ServiceCall call, Book request) async {
    var book = Book();
    book.title = request.title;
    book.id = request.id;
    books.books.add(book);
    return book;
  }

  @override
  Future<Books> getAllBooks(ServiceCall call, Empty request) async {
    return books;
  }

  @override
  Future<Book> getBook(ServiceCall call, BookId request) async {
    var book = books.books.firstWhere((book) => book.id == request.id);
    return book;
  }

  @override
  Future<Empty> deleteBook(ServiceCall call, BookId request) async {
    books.books.removeWhere((book) => book.id == request.id);
    return Empty();
  }

  @override
  Future<Book> editBook(ServiceCall call, Book request) async {
    var book = books.books.firstWhere((book) => book.id == request.id);
    book.title = request.title;
    return book;
  }
}

Future<void> main(List<String> args) async {
  final server = Server(
    [BookMethodsService()],
    const <Interceptor>[],
    CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),
  );
  await server.serve(port: 50051);
  print('Server listening on port ${server.port}...');
}

What a chunk of code! It looks daunting, but it’s simpler than you might think.

The first part imports the required files. We imported the grpc code and grpc Dart code. We imported the book.pb.dart and book.pbgrpc.dart files because we need the classes therein.

Below, we extend the BookMethodsServiceBase interface in BookMethodsService to provide the implementations for all the methods in the BookMethods service.

In the BookMethodsService class, we override all the methods to provide their implementations. Notice the two parameters in the methods. The first param, ServiceCall call, contains meta-information on the request. The second param holds the info that is sent, which is the type of data the rpc method will accept as an argument.

  Books books = Books();

The above command sets a books array.

In the createBook method, we created a new Book, set the id, title, and added it to the books array in the books variable.

In the getAllBooks method, we just returned the books variable.

In the getBook method, we fetched the ID from the BookId request object and used it to get the book from the books array using the List#firstWhere method and return it.

In deleteBook, we oet the bookID from the BookId request and used it as cursor to remove the book from the books array using the List#removeWhere method.

In the editBook method, the request arg contains the Book info. We retrieved the book from the books array and edited its title property value to the one sent in the request arg.

Finally, we set up the server in the main function. We passed the BookMethodsService instance in an array to the Server constructor. Then, we called the serve method to start the server at port 50051.

Now let’s build the client.

Building a gRPC client

Create a client.dart file inside the bin folder:

touch bin/client.dart

Open it and paste the following code:

import 'package:grpc/grpc.dart';
import './../book.pb.dart';
import './../book.pbgrpc.dart';

class Client {
  ClientChannel channel;
  BookMethodsClient stub;

  Future<void> main(List<String> args) async {
    channel = ClientChannel('localhost',
        port: 50051,
        options: // No credentials in this example
            const ChannelOptions(credentials: ChannelCredentials.insecure()));
    stub = BookMethodsClient(channel,
        options: CallOptions(timeout: Duration(seconds: 30)));
    try {
      //...
      var bookToAdd1 = Book();
      bookToAdd1.id = 1;
      bookToAdd1.title = "Things Fall Apart";
      var addedBook1 = await stub.createBook(bookToAdd1);
      print("Added a book: " + addedBook1.toString());

      var bookToAdd2 = Book();
      bookToAdd2.id = 2;
      bookToAdd2.title = "No Longer at Ease";
      var addedBook2 = await stub.createBook(bookToAdd2);
      print("Added a book: " + addedBook2.toString());

      var allBooks = await stub.getAllBooks(Empty());
      print(allBooks.books.toString());

      var bookToDel = BookId();
      bookToDel.id = 2;
      await stub.deleteBook(bookToDel);
      print("Deleted Book with ID: " + 2.toString());

      var allBooks2 = await stub.getAllBooks(Empty());
      print(allBooks2.books);

      var bookToEdit = Book();
      bookToEdit.id = 1;
      bookToEdit.title = "Beware Soul Brother";
      await stub.editBook(bookToEdit);

      var bookToGet = BookId();
      bookToGet.id = 1;
      var bookGotten = await stub.getBook(bookToGet);
      print("Book Id 1 gotten: " + bookGotten.toString());
    } catch (e) {
      print(e);
    }
    await channel.shutdown();
  }
}

main() {
  var client = Client();
  client.main([]);
}

We imported the grpc.dart package and the book.pb.dart and book.pbgrpc.dart files. We created a class Client class. We have a BookMethodsClient stub; the stub will hold the BookMethodsClient instance, which is where we can call the BookMethods service methods to invoke them in the server.

In the main method, we created a ClientChannel instance and also a BookMethodsClient instance pass in the ClientChannel instance to its constructor. BookMethodsClient uses the instance to get config — for example, the port the gRPC server will be reached on. In our case, it’s 50051 and the timeout time.

Inside the try statement body, we called our gPRC methods. First, we created a book with the title “Things Fall Apart” and assigned it an ID of 1. We called the createBook method in the stub, passing in the Book instance bookToAdd1 to the method as arg. This will call the createBook method in the server with the addToAdd1 object.

Next, we created a new book instance, “No Longer at Ease,” with the ID 2 and called the createBook method, passing in the book instance. This remotely invoked the createBook method in the gRPC server and a new book was created.

We called the getAllBooks method to get all books on the server.

Next, we set up a BooKId object, setting its id to 2. Then, we called the deleteBook method,
passing in the BookId object. This deletes the book with id 2 (“No Longer at Ease”) from the server.

Notice where we edit a book. We created a BookId instance with an ID set to 1 and a title set to “Beware Soul Brother.” We want to edit the title of the book with ID 1 to say “Beware Soul Brother” instead of “Things Fall Apart.” So we called the editBook method, passing in the BookId instance.

Last, we retrieved a specific book using its ID. We created a BookId instance with its id set to 1. This means we want to get the book with the ID of 1, which represents the book “Beware Soul Brother.” So, we called the getBook method, passing the BookId instance. The return should be a Book object with the title “Beware Soul Brother.”

After all this, the channel is shut down by calling the shutdown method in ClientChannel from its channel instance.

Testing the server

Now it’s time to test everything. First, run the server:

➜  dart_grpc dart bin/dart_grpc.dart
Server listening on port 50051...

Open another terminal and run the client:

➜  dart_grpc dart bin/client.dart
Added a book: id: 1
title: Things Fall Apart

Added a book: id: 2
title: No Longer at Ease

[id: 1
title: Things Fall Apart
, id: 2
title: No Longer at Ease
]
Deleted Book with ID: 2
[id: 1
title: Things Fall Apart
]
Book Id 1 gotten: id: 1
title: Beware Soul Brother

➜  dart_grpc

That’s it — our gRPC server is working as intended!

The complete source code for this example is available on GitHub.

Conclusion

We covered a lot in this tutorial. We started by introducing gRPC generally and explaining how it works from the protocol buffers down to the client.

Next, we demonstrated how to install tools and plugins for the protocol buffer compiler. These are used to generate Dart source code from the proto definitions. After that, we walked through the process of creating an actual gRPC service in Dart, building a gRPC client, and calling the methods from the client. Finally, we tested everything and found that it works great.

gRPC is very powerful and there is a lot more you can discover by playing around with it yourself. The examples in this tutorial should leave you with a solid foundation.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Chidume Nnamdi I'm a software engineer with over six years of experience. I've worked with different stacks, including WAMP, MERN, and MEAN. My language of choice is JavaScript; frameworks are Angular and Node.js.

Leave a Reply