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.
Project Initialization
Create the project
Create a demo directory and switch into it:
mkdir demo && cd demoInitialize the project and install gRPCity (requires Node.js >= 18):
npm init -y
npm i grpcityAdd "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:
.
├── client.js
├── loader.js
├── package-lock.json
├── package.json
├── proto
│ └── helloworld
│ ├── model
│ │ └── message.proto
│ └── service.proto
└── server.jsLoad Proto
Define the proto
Two services — Greeter and Hellor — share a model package for messages.
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:
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:
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
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.
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:
class Hellor {
async sayHello(call) {
const { name } = call.request
return { message: `hello ${name || "world"} by Hellor` }
}
}Bind and Start
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
import loader from "./loader.js"Get the client
Build a start function that initialises the loader and a clients factory.
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:
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
node ./server.js
helloworld server is started: 127.0.0.1:9098Once 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:
node ./client.js
greeterClient.sayGreet { message: 'hello grpcity by Greeter', count: 1 }
hellorClient.sayHello { message: 'hello grpcity by Hellor' }Second run:
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 .