DEV Community

Diego Liascovich
Diego Liascovich

Posted on

Building a Clean gRPC API in Node.js

By Diego Liascovich

Full-Stack Developer | Microservices | Angular | Node.js

gRPC is a high-performance, language-agnostic RPC framework developed by Google. It allows different services or applications—possibly written in different languages—to communicate through well-defined contracts using Protocol Buffers.

In this post, we'll cover:

  • What gRPC is and how it works
  • Why it's useful in modern backend architectures
  • How to implement a simple BookService API using Node.js, gRPC, Docker, and Clean Architecture principles

🚀 What Is gRPC?

gRPC stands for gRPC Remote Procedure Calls. It uses HTTP/2 for transport, Protocol Buffers (protobuf) as the interface definition language, and supports multiple languages.

🧱 Components of gRPC

  • .proto file: Defines services and message schemas
  • Server: Implements service logic
  • Client: Invokes remote methods
  • Protocol Buffers: Efficient binary serialization format

⚙️ Why Use gRPC?

  • ✅ Strongly-typed messages (via Protobuf)
  • ✅ Efficient binary communication
  • ✅ Multi-language support (Node, Go, Java, Python, etc.)
  • ✅ Streaming support (client/server/bidirectional)
  • ✅ Ideal for microservices in internal networks

📘 Our Use Case: Book Service

We’ll build a simple BookService API with two RPC methods:

  • GetBook(id): returns a book by ID
  • ListBooks(): returns all books

All data will be mocked in memory, simulating a database.


🗂️ Project Structure

grpc-books/ ├── proto/ │ └── book.proto ├── proto-loader.js ├── src/ │ ├── application/ │ │ └── bookService.js │ ├── domain/ │ │ └── book.js │ ├── infrastructure/ │ │ └── bookRepository.js │ ├── interfaces/ │ │ └── grpc/ │ │ └── bookHandler.js │ └── server.js ├── Dockerfile ├── docker-compose.yml └── package.json 
Enter fullscreen mode Exit fullscreen mode

📄 book.proto

syntax = "proto3"; package books; service BookService { rpc GetBook (GetBookRequest) returns (Book); rpc ListBooks (Empty) returns (BookList); } message GetBookRequest { string id = 1; } message Empty {} message Book { string id = 1; string title = 2; string author = 3; int32 year = 4; } message BookList { repeated Book books = 1; } 
Enter fullscreen mode Exit fullscreen mode

🧠 Domain: Book Entity

// src/domain/book.js class Book { constructor(id, title, author, year) { this.id = id; this.title = title; this.author = author; this.year = year; } } module.exports = Book; 
Enter fullscreen mode Exit fullscreen mode

🏗️ Infrastructure: Mock Repository

// src/infrastructure/bookRepository.js const Book = require('../domain/book'); const books = [ new Book('1', '1984', 'George Orwell', 1949), new Book('2', 'The Hobbit', 'J.R.R. Tolkien', 1937), new Book('3', 'Clean Code', 'Robert C. Martin', 2008), ]; class BookRepository { findById(id) { return books.find(b => b.id === id); } findAll() { return books; } } module.exports = new BookRepository(); 
Enter fullscreen mode Exit fullscreen mode

💼 Application: Book Service

// src/application/bookService.js const bookRepo = require('../infrastructure/bookRepository'); class BookService { getBook(id) { const book = bookRepo.findById(id); if (!book) throw new Error('Book not found'); return book; } listBooks() { return bookRepo.findAll(); } } module.exports = new BookService(); 
Enter fullscreen mode Exit fullscreen mode

🔌 Interface: gRPC Handler

// src/interfaces/grpc/bookHandler.js const bookService = require('../../application/bookService'); function GetBook(call, callback) { try { const book = bookService.getBook(call.request.id); callback(null, book); } catch (err) { callback({ code: 5, message: err.message }); // NOT_FOUND } } function ListBooks(call, callback) { const books = bookService.listBooks(); callback(null, { books }); } module.exports = { GetBook, ListBooks }; 
Enter fullscreen mode Exit fullscreen mode

📦 Proto Loader

// proto-loader.js const path = require('path'); const grpc = require('@grpc/grpc-js'); const protoLoader = require('@grpc/proto-loader'); const PROTO_PATH = path.join(__dirname, 'proto', 'book.proto'); const packageDefinition = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, }); module.exports = grpc.loadPackageDefinition(packageDefinition).books; 
Enter fullscreen mode Exit fullscreen mode

🖥️ Server

// src/server.js const grpc = require('@grpc/grpc-js'); const proto = require('../proto-loader'); const bookHandler = require('./interfaces/grpc/bookHandler'); function main() { const server = new grpc.Server(); server.addService(proto.BookService.service, { GetBook: bookHandler.GetBook, ListBooks: bookHandler.ListBooks, }); const address = '0.0.0.0:50051'; server.bindAsync(address, grpc.ServerCredentials.createInsecure(), () => { console.log(`🚀 gRPC server running at ${address}`); server.start(); }); } main(); 
Enter fullscreen mode Exit fullscreen mode

🐳 Docker Setup

Dockerfile

FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 50051 CMD ["npm", "start"] 
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml

version: "3.9" services: grpc-books: build: . ports: - "50051:50051" 
Enter fullscreen mode Exit fullscreen mode

🧪 Testing with BloomRPC

  1. Open BloomRPC.
  2. Load the book.proto file.
  3. Set server to localhost:50051.
  4. Use:
    • GetBook → payload: { "id": "1" }
    • ListBooks → payload: {}

📁 Source Code

👉 View the full repository here

Top comments (0)