Skip to Content
🎉 gRPCity 3.0 is released. Read more →
DocumentationGet Started

Get Started

This page walks through a complete request/response loop in four steps: project setup, the loader, the server, and the client. Some prior familiarity with gRPC and protobuf helps but isn’t required — the snippets below are runnable as-is.

Background reading: gRPC  and proto3 .

Project Initialization

Create the project

Create a demo directory and switch into it:

Terminal
mkdir demo && cd demo

Initialize the project and install gRPCity (requires Node.js >= 18):

Terminal
npm init -y npm i grpcity

Add "type": "module" to package.json so the snippets below run as ESM.

Project layout

The final tree looks like this. The highlighted lines are files you’ll create as you go:

Terminal
. ├── client.js ├── loader.js ├── package-lock.json ├── package.json ├── proto │ └── helloworld │ ├── model │ │ └── message.proto │ └── service.proto └── server.js

Load Proto

Define the proto

Two services — Greeter and Hellor — share a model package for messages.

service.proto:

./proto/helloworld/service.proto
syntax = "proto3"; package helloworld; import "helloworld/model/message.proto"; service Greeter { rpc SayGreet(HelloRequest) returns (HelloReply) {} } service Hellor { rpc SayHello(HelloRequest) returns (HelloReply) {} }

Enter the following content for message.proto:

./proto/helloworld/model/message.proto
syntax = "proto3"; package helloworld.model; message HelloRequest { string name = 1; } message HelloReply { string message = 1; int32 count = 2; }

Write the loader

A single loader instance is shared by the server and the client, so the proto files are parsed exactly once.

./loader.js:

./loader.js
import { ProtoLoader } from 'grpcity' import path from 'node:path' // __dirname for esm import { fileURLToPath } from 'node:url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) export default new ProtoLoader({ location: path.join(__dirname, './proto'), files: [ 'helloworld/service.proto' ] })

location is the root directory the loader searches; files are the proto files inside it.

That’s it for the loader. Any module that needs to build clients or register services can import ./loader.js.

Implement the server

Everything below goes into ./server.js.

Import the loader

./server.js
import loader from "./loader.js"

Implement Greeter

Each service is an ordinary class. Methods receive the call (with request, metadata, helpers) and return the response object.

./server.js
class Greeter { constructor() { this.count = 0 } async sayGreet(call) { const { name } = call.request this.count++ return { message: `hello ${name || "world"} by Greeter`, count: this.count } } }

Implement Hellor

Same pattern, second service:

./server.js
class Hellor { async sayHello(call) { const { name } = call.request return { message: `hello ${name || "world"} by Hellor` } } }

Bind and Start

./server.js
const start = async (addr) => { // loader initialization await loader.init() // server initialization and get instance const server = await loader.initServer() // bind class methods with service server.add('helloworld.Greeter', new Greeter()) server.add('helloworld.Hellor', new Hellor()) // listen await server.listen(addr) console.log('helloworld server is started: ', addr) } // start start('127.0.0.1:9098')

Implement the client

Import the loader

./client.js
import loader from "./loader.js"

Get the client

Build a start function that initialises the loader and a clients factory.

./client.js
const start = async (addr) => { await loader.init() const clients = await loader.initClients({ services: { 'helloworld.Greeter': addr, 'helloworld.Hellor': addr } }) }

Make the calls

Round out start with two RPCs and print what each returns:

./client.js
const start = async (addr) => { // ... // loader init // clients init // .... // greeter client const greeterClient = clients.get('helloworld.Greeter') const greeterResult = await greeterClient.sayGreet({ name: 'grpcity' }) console.log('greeterClient.sayGreet', greeterResult.response) // hellor client const hellorClient = clients.get('helloworld.Hellor') const hellorResult = await hellorClient.sayHello({ name: 'grpcity' }) console.log('hellorClient.sayHello', hellorResult.response) } // execute start('127.0.0.1:9098')

Every client call resolves to { status, peer, metadata, response }. We only print response here, but the other three fields are useful for logging and tracing.

Run it

Two terminals: server in one, client in the other.

Start the server

Terminal
node ./server.js helloworld server is started: 127.0.0.1:9098

Once it’s listening, the client can dial that address.

Start the client

Run the client twice and watch the in-memory counter on the server side bump:

First run:

Terminal
node ./client.js greeterClient.sayGreet { message: 'hello grpcity by Greeter', count: 1 } hellorClient.sayHello { message: 'hello grpcity by Hellor' }

Second run:

Terminal
node ./client.js greeterClient.sayGreet { message: 'hello grpcity by Greeter', count: 2 } hellorClient.sayHello { message: 'hello grpcity by Hellor' }

The count field increments between runs — state lives in the Greeter instance, exactly as you’d expect from a long-running server.

That’s the whole loop. Read on in the User Guide for streaming, middleware, TLS, and beyond.

The full source for this walk-through lives at chakhsu/grpcity-basic-demo .

Last updated on