DEV Community

Cover image for Creating a CRUD API with Node, Express, and gRPC
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Creating a CRUD API with Node, Express, and gRPC

Written by Diogo Souza✏️

Speed is becoming increasingly valuable in the web world.

Developers launching new releases of softwares, frameworks and libraries have certainly invested a lot of time in decreasing numbers regarding loading time, request processing, and resources consuming.

HTTP/2, for example, was born through a myriad of optimizations that led the web to a more robust, faster, and lighter zone than we’ve ever been before.

The RPC (that stands for Remote Procedure Call) is a well-known way to get traction when you need to be a bit remote or distributed. In the era of enterprise servers and complicated massive amounts of code needed to set things up, it used to reign.

After years of isolation, Google redesigned it and has put new light into it.

gRPC is a modern open source high performance RPC framework that can run in any environment.

It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking, and authentication.

It is also applicable in the last mile of distributed computing to connect devices, mobile applications, and browsers to back-end services.

It’s backed in HTTP/2, cross platform, and open source. It’s also compact in regards to its size.

gRPC works with many programming languages like Java, Go, Ruby, Python and more.

Go ahead and check their official documentation link (as well as its GitHub page) to check if there’s support for yours.

Even if your language is not listed there, you can make use of the web features in a Docker image.

This is how its workflow looks:

gRPC
gRPC Workflow

The whole architecture is based in the known client-server structure.

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.

The server assures that the interface will be provided by its services, while the client has stubs to guarantee that the methods are alike.

It also uses the Protocol Buffer to serialize and deserialize request and response data, instead of JSON or XML, for example.

Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data — think XML, but smaller, faster, and simpler.

You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams in each of the supported languages.

LogRocket Free Trial Banner

First, you need to create and define the protobuf file, which will contain code made under the Interface Definition Language specified by the protocol itself (more on that later).

With the file in hand, you can compile it via protoc compiler to the desired language code.

This whole process is made under the hood, so don’t worry, you won’t see lots of boilerplate code around. In the end, along with the generated code, you can go to the implementation of the server and client.

Rather than imagining, we’re going to build a fully-functional CRUD API application with a Bootstrap interface to manage the operations of an in-memory list of customers (to be managed by the server app).

This is how our application will look in the end:

Logrocket customers list CRUD
Customer’s CRUD application

Setup

The requirements for this tutorial are very simple:

  • Node.js and npm (latest versions)
  • The IDE of your choice

For the sake of simplicity, we’re not going to use any sort of database — the list of items will be kept in memory in the server application.

This will simulate very closely the use of a database, since the data will be there when the server is up, while the client can be restarted as many times as you wish. Feel free to incorporate whatever framework or features you want.

Next, in a folder of your choice, create the following structure of folders and files:

project structure
Project’s structure

You can also opt to create client and server applications separately.

We kept them together to simplify the final structure.

Now, run the following command at the root folder in the command line:

npm install --save grpc @grpc/proto-loader uuid express hbs body-parser
Enter fullscreen mode Exit fullscreen mode

The first two installs will handle the gRPC server and the load of our protobuf file to the implementation of both client and server codes. Uuid will be useful for creating random hash ids for our customers, but you can use numbers as well to simplify (although this way your code would already be prepared to switch to a MongoDB, for example).

You may be wondering why we’re using Express (for HTTP handling) here if we’re developing an API under a different protocol.

Express will just serve the routing system for our view. Each CRUD operation will need to get to the client (which is a HTTP server, by the way) that, in turn, will communicate via gRPC with the server application.

While you can call gRPC methods from a web page, I wouldn’t recommend it since there’s a lot of cons.

Remember, gRPC was made to speed up things in the back-end, like from a microservice to another. In order to serve to a front page, mobile apps, or any other types of GUIs, you have to adapt your architecture.

Finally, we have Handlebars for the templating of our page (we won’t cover details on it here, but you can use EJS or any other templating system for Node apps), and body-parser for converting the incoming request bodies in a middleware before your handlers, available under the req.body property.

It’s going to make our life easier when accessing request params.

Your final package.json file should look like this:

{
  "name": "logrocket_customers_grpc",
  "version": "1.0.0",
  "description": "LogRocket CRUD with gRPC and Node",
  "main": "server.js",
  "scripts": {
    "start": "node server/server.js"
  },
  "author": "Diogo Souza",
  "license": "MIT",
  "dependencies": {
    "@grpc/proto-loader": "^0.5.3",
    "body-parser": "^1.18.3",
    "express": "^4.17.1",
    "grpc": "^1.24.2",
    "hbs": "^4.1.0",
    "uuid": "^7.0.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

The server

Let’s move to the code, starting with our protobuf file, customers.proto:

syntax = "proto3";

service CustomerService {
    rpc GetAll (Empty) returns (CustomerList) {}
    rpc Get (CustomerRequestId) returns (Customer) {}
    rpc Insert (Customer) returns (Customer) {}
    rpc Update (Customer) returns (Customer) {}
    rpc Remove (CustomerRequestId) returns (Empty) {}
}

message Empty {}

message Customer {
    string id = 1;
    string name = 2;
    int32 age = 3;
    string address = 4;
}

message CustomerList {
    repeated Customer customers = 1;
}

message CustomerRequestId {
    string id = 1;
}
Enter fullscreen mode Exit fullscreen mode

The first line states the version of protobuf we’ll use — in this case, the latest one.

The syntax of the content reassembles a lot of JSON. The service is the interface contract we’ve talked about. Here you’ll place the method names, params, and return types of each gRPC call.

The types, when not a primitive one, must be stated through the message keyword. Please refer to the docs to see all the allowed types.

Each of a message’s properties has to receive a number value that represents the order of this property in the stack, starting with 1.

Finally, for arrays, you need to use the repeated keyword before the declaration’s property.

With the proto in hand, let’s create our server.js code:

const PROTO_PATH = "./customers.proto";

var grpc = require("grpc");
var protoLoader = require("@grpc/proto-loader");

var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    arrays: true
});

var customersProto = grpc.loadPackageDefinition(packageDefinition);

const { v4: uuidv4 } = require("uuid");

const server = new grpc.Server();
const customers = [
    {
        id: "a68b823c-7ca6-44bc-b721-fb4d5312cafc",
        name: "John Bolton",
        age: 23,
        address: "Address 1"
    },
    {
        id: "34415c7c-f82d-4e44-88ca-ae2a1aaa92b7",
        name: "Mary Anne",
        age: 45,
        address: "Address 2"
    }
];

server.addService(customersProto.CustomerService.service, {
    getAll: (_, callback) => {
        callback(null, { customers });
    },

    get: (call, callback) => {
        let customer = customers.find(n => n.id == call.request.id);

        if (customer) {
            callback(null, customer);
        } else {
            callback({
                code: grpc.status.NOT_FOUND,
                details: "Not found"
            });
        }
    },

    insert: (call, callback) => {
        let customer = call.request;

        customer.id = uuidv4();
        customers.push(customer);
        callback(null, customer);
    },

    update: (call, callback) => {
        let existingCustomer = customers.find(n => n.id == call.request.id);

        if (existingCustomer) {
            existingCustomer.name = call.request.name;
            existingCustomer.age = call.request.age;
            existingCustomer.address = call.request.address;
            callback(null, existingCustomer);
        } else {
            callback({
                code: grpc.status.NOT_FOUND,
                details: "Not found"
            });
        }
    },

    remove: (call, callback) => {
        let existingCustomerIndex = customers.findIndex(
            n => n.id == call.request.id
        );

        if (existingCustomerIndex != -1) {
            customers.splice(existingCustomerIndex, 1);
            callback(null, {});
        } else {
            callback({
                code: grpc.status.NOT_FOUND,
                details: "Not found"
            });
        }
    }
});

server.bind("127.0.0.1:30043", grpc.ServerCredentials.createInsecure());
console.log("Server running at http://127.0.0.1:30043");
server.start();
Enter fullscreen mode Exit fullscreen mode

Since it’s a server, it looks a lot like the structure of an Express code, for example. You have an IP and a port, and then you start something up.

Some important points:

First, import the proto file path to a constant.

Then, require both grpc and @grpc/proto-loader packages. They’re the ones that’ll make the magic happen. In order to have a proto transcripted into a JavaScript object, you need to set its package definition first. protoLoader will take care of this task by receiving the path where the proto file is located as the first param, and the setting properties as the second.

Once you have the package definition object in hand, you pass it over to the loadPackageDefinition function of grpc object that, in turn, will return it to you. Then, you can create the server via Server() function.

The customers array is our in-memory database.

We’re initializing it with two customers already so you can see some data when the apps start up. On the server, we need to tell the server object which services it’ll take care of (in our case, the CustomerService we’ve created in the proto file). Each of the operations must match their names with the proto ones respectively. Their codes are easy and very straightforward, so go ahead and take a look at them.

In the end, bind the server connection to the desired IP and port and start it up. The bind() function received the authentication object as the second parameter, but for simplicity we’ll use it insecurely as you may notice (not recommended for production).

The server’s done. Simple, isn’t it? You can now start it up by issuing the following command:

npm start
Enter fullscreen mode Exit fullscreen mode

However, it can’t be tested because you need a proper client that understands the protobuf contract the server serves.

The client

Let’s build our client application now, starting with the client.js code:

const PROTO_PATH = "../customers.proto";

const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");

var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    arrays: true
});

const CustomerService = grpc.loadPackageDefinition(packageDefinition).CustomerService;
const client = new CustomerService(
    "localhost:30043",
    grpc.credentials.createInsecure()
);

module.exports = client;
Enter fullscreen mode Exit fullscreen mode

This file will exclusively handle our communication with the gRPC server.

Note that its initial structure is exactly the same as in the server file because the same gRPC objects handle the client and server instances.

The only difference here is that there’s no such method like Client().

All we need is to load the package definition and create a new service — the same one we’ve created in the server — over the same IP and port. If you have credentials set, the second param must meet the settings as well.

That’s it.

To use this service contract, we need first to implement our Express code. So, in the index.js file, insert the following:

const client = require("./client");

const path = require("path");
const express = require("express");
const bodyParser = require("body-parser");

const app = express();

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "hbs");

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.get("/", (req, res) => {
    client.getAll(null, (err, data) => {
        if (!err) {
            res.render("customers", {
                results: data.customers
            });
        }
    });
});

app.post("/save", (req, res) => {
    let newCustomer = {
        name: req.body.name,
        age: req.body.age,
        address: req.body.address
    };

    client.insert(newCustomer, (err, data) => {
        if (err) throw err;

        console.log("Customer created successfully", data);
        res.redirect("/");
    });
});

app.post("/update", (req, res) => {
    const updateCustomer = {
        id: req.body.id,
        name: req.body.name,
        age: req.body.age,
        address: req.body.address
    };

    client.update(updateCustomer, (err, data) => {
        if (err) throw err;

        console.log("Customer updated successfully", data);
        res.redirect("/");
    });
});

app.post("/remove", (req, res) => {
    client.remove({ id: req.body.customer_id }, (err, _) => {
        if (err) throw err;

        console.log("Customer removed successfully");
        res.redirect("/");
    });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log("Server running at port %d", PORT);
});
Enter fullscreen mode Exit fullscreen mode

After you’ve imported the requires, created the app from express() function and set each of the CRUD HTTP functions, what’s left is just the call for each of the actions provided by the interface contract.

Note also that, for all of them, we’re recovering the input values from the request body (courtesy of body-parser).

Don’t forget that each client function must meet the exact same name as was defined in the proto file.

Last but not least, this is the code for customers.hbs file:

<html lang="en">

<head>
    <meta charset="utf-8">
    <title>LogRocket CRUD with gRPC and Node</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <style>
        .logrocket {
            background-color: #764abc;
            color: white;
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="py-5 text-center">
            <img class="d-block mx-auto mb-4"
                src="https://blog.logrocket.com/wp-content/uploads/2020/01/logrocket-blog-logo.png" alt="Logo"
                height="72">
            <h2>Customer's List</h2>
            <p class="lead">Example of CRUD made with Node.js, Express, Handlebars and gRPC</p>
        </div>

        <table class="table" id="customers_table">
            <thead>
                <tr>
                    <th>Customer ID</th>
                    <th>Customer Name</th>
                    <th>Age</th>
                    <th>Address</th>
                    <th>Action</th>
                </tr>
            </thead>
            <tbody>
                {{#each results}}
                <tr>
                    <td>{{ id }}</td>
                    <td>{{ name }}</td>
                    <td>{{ age }} years old</td>
                    <td>{{ address }}</td>
                    <td>
                        <a href="javascript:void(0);" class="btn btn-sm edit logrocket" data-id="{{ id }}"
                            data-name="{{ name }}" data-age="{{ age }}" data-address="{{ address }}">Edit</a>
                        <a href="javascript:void(0);" class="btn btn-sm btn-danger remove" data-id="{{ id }}">Remove</a>
                    </td>
                </tr>
                {{else}}
                <tr>
                    <td colspan="5" class="text-center">No data to display.</td>
                </tr>
                {{/each}}
            </tbody>
        </table>
        <button class="btn btn-success float-right" data-toggle="modal" data-target="#newCustomerModal">Add New</button>
    </div>

    <!-- New Customer Modal -->
    <form action="/save" method="post">
        <div class="modal fade" id="newCustomerModal" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">New Customer</h4>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <div class="form-group">
                            <input type="text" name="name" class="form-control" placeholder="Customer Name"
                                required="required">
                        </div>

                        <div class="form-group">
                            <input type="number" name="age" class="form-control" placeholder="Age" required="required">
                        </div>

                        <div class="form-group">
                            <input type="text" name="address" class="form-control" placeholder="Address"
                                required="required">
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="submit" class="btn logrocket">Create</button>
                    </div>
                </div>
            </div>
        </div>
    </form>

    <!-- Edit Customer Modal -->
    <form action="/update" method="post">
        <div class="modal fade" id="editCustomerModal" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">Edit Customer</h4>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <div class="form-group">
                            <input type="text" name="name" class="form-control name" placeholder="Customer Name"
                                required="required">
                        </div>

                        <div class="form-group">
                            <input type="number" name="age" class="form-control age" placeholder="Age"
                                required="required">
                        </div>

                        <div class="form-group">
                            <input type="text" name="address" class="form-control address" placeholder="Address"
                                required="required">
                        </div>
                    </div>
                    <div class="modal-footer">
                        <input type="hidden" name="id" class="customer_id">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="submit" class="btn logrocket">Update</button>
                    </div>
                </div>
            </div>
        </div>
    </form>

    <!-- Remove Customer Modal -->
    <form id="add-row-form" action="/remove" method="post">
        <div class="modal fade" id="removeCustomerModal" role="dialog" aria-labelledby="myModalLabel">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title"></h4>Remove Customer</h4>
                        <button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
                    </div>
                    <div class="modal-body">
                        Are you sure?
                    </div>
                    <div class="modal-footer">
                        <input type="hidden" name="customer_id" class="form-control customer_id_removal"
                            required="required">
                        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                        <button type="submit" class="btn logrocket">Remove</button>
                    </div>
                </div>
            </div>
        </div>
    </form>

    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
        integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
        crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
        integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
        crossorigin="anonymous"></script>
    <script>
        $(document).ready(function () {
            $('#customers_table').on('click', '.edit', function () {
                $('#editCustomerModal').modal('show');

                $('.customer_id').val($(this).data('id'));
                $('.name').val($(this).data('name'));
                $('.age').val($(this).data('age'));
                $('.address').val($(this).data('address'));
            }).on('click', '.remove', function () {
                $('#removeCustomerModal').modal('show');

                $('.customer_id_removal').val($(this).data('id'));
            });
        });
    </script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

It’s a little lengthy, especially because I decided to create the whole CRUD UIs into the same page, via Bootstrap modals, rather than redirecting and setting up a lot of different pages.

By the beginning and the end of the page, we find the imports for the CSS and JS files of Bootstrap and jQuery, respectively.

The main table is making use of Handlebars foreach instruction via:

{{#each results}}

{{else}}

{{/each}}
Enter fullscreen mode Exit fullscreen mode

The else here helps to configure a text for when no elements are available to the listing. Regarding the links of editing and removing operations, we’re setting HTML data attributes to help out with the modals when they open.

Every time we open the edit a modal, each of its inputs have to be filled with the correspondent value for that row’s values. The same goes for the removing action, even though here we only need the id.

At the end of the first div, we can see the link for adding new customers, which also triggers the respective modal.

Right below, there are the three modals.

They’re very similar to each other, since they only hold the HTML structure.

The logic is actually going to be placed at the JavaScript section that comes at the end of the HTML.

Here, we’re using jQuery to open the modal itself, and to facilitate the work of changing the values (via val function) of each modal’s input to their corresponding data attribute value.

We’re done. Now you can start the client in another command line window by issuing:

node index
Enter fullscreen mode Exit fullscreen mode

Then, with the server also up, go to the http://localhost:3000/ and test it out.

Conclusion

You can find the final source code of this project here.

Now you can deploy it to the cloud or a production server, or start with a modest POC in your own projects to see how fast it performs compared to your REST APIs.

But there’s a lot more you can do with gRPC. You can insert authentication to make it safer, timeouts, bidirectional streaming, robust error handling mechanisms, channeling, and more.

Make sure to read the docs to check more of its power.


200's only ‎✅: Monitor failed and show GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.


The post Creating a CRUD API with Node, Express, and gRPC appeared first on LogRocket Blog.

Top comments (0)