Using gRPC with NestJS and Angular
Background
If you haven’t already heard, gRPC is gaining a lot of traction in the development community at the moment. gRPC is an open-source remote procedure call standard which supports authentication and streaming of data between servers and clients. It originated at Google from their internal tool Stubby, which is used to power many Google services today.
If you are interested to learn more about gRPC, Microsoft has a good comparison between gRPC and traditional REST APIs, and there are some great posts showing benchmarks between gRPC traditional REST APIs. In some cases gRPC reduces processing time by 50%. So in short, it’s awesome and you should consider it for high-end performance applications.
It’s well documented how to implement gRPC for C-lang, Java, and other server-side languages since it’s most commonly used for high-performance micro-service architectures. There are already some great tutorials for using gRPC with NodeJS:
But what about using gRPC with TypeScript? with NestJS and Angular? Is it even possible? Here how I achieved it.
Using gRPC with NestJS
First off, we want to install NestJS and then create a new project using the command line tool:
npm i -g @nestjs/cli
nest new nest-grpc
cd nest-grpc
npm run start
Then install the gRPC proto-loader dependencies and NestJS microservices:
npm i --save grpc @grpc/proto-loader @nestjs/microservices
I’m not going to show every single step of creating the example endpoints, as the official guide already shows this, and is kept up-to-date with NestJS releases. Follow the steps from the guide here:
https://docs.nestjs.com/microservices/grpc
If you want to skip these steps, you can clone the official NestJS gRPC example from Github directly:
https://github.com/nestjs/nest/tree/master/sample/04-grpc
Once completed, you should have a gRPC service running using NestJS.
Testing your gRPC endpoints
Normal browser tools don’t natively support gRPC calls. You can explore and test your endpoints using grpcc:
npm install -g grpcc
Connect to your gRPC server on the correct port. Note: NestJS defaults to port 5000, but depending on your configuration it could be different:
grpcc -i --proto ./src/hero/hero.proto --address localhost:5000
Then call your endpoints using:
// call methods
client.getHeroes({}, printReply)
client.getHeroById({ id: 1 }, printReply)// call streaming methods
var call = client.getHeroesStream().on('data', streamReply).on('status', streamReply); call.write({});
var call = client.getHeroByIdStream().on('data', streamReply).on('status', streamReply); call.write({ id: 1 });
One important thing to note is that data has to be returned as an object. So you’ll noticed the array of heroes is nested inside an object called “heroes”.
The Github version of the NestJS server also exposes a REST API. You can use compare the two approaches REST vs gRPC, by accessing it in the browser:
http://localhost:3001/hero
http://localhost:3001/hero/1
Using gRPC with Angular
Now our backend server is serving gRPC endpoints, how can we access them via the frontend? So it turns out that gRPC is not natively supported in the browser the explanation is:
For gRPC-Web to work, we need a lot of the underlying transport to be exposed to us but that’s not the case currently cross browsers. We cannot leverage the full http2 protocol given the current set of browser APIs. gRPC relies on trailing headers but browsers don’t expose those.
Therefore we need two additional steps before we can use gRPC in the browser:
- Generate client-side .proto stubs
- Use a proxy to support requests
Lets start off by creating a new Angular project:
npm install -g @angular/cli
ng new angular-grpc
cd angular-grpc
npm start
We need some additional tools to generate and load client-side .proto stubs for Angular. Install using the command:
npm install @improbable-eng/grpc-web google-protobuf grpc protoc ts-protoc-gen
Next we need to add a compile script to our Angular package.json to convert the .proto files using our tools:
{
"name": "angular-grpc",
"version": "0.0.2",
"description": "Angular gRPC example",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0",
"build": "ng build",
"compile": "./node_modules/protoc/protoc/bin/protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:src/app/proto --ts_out=service=true:src/app/proto -I ../backend/src/ ../backend/src/**/*.proto",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
}
}
Every time your .proto files change, you need to regenerate the client-side stubs using this shortcut compile command:
npm run compile
After running the command, you should see the JavaScript and TypeScript definitions for your .proto model:
One benefit of this, is that all our Types are created for us. No more manual updating of data types and services.
To use them, simply import the service to your Controller or Service and call the appropriate method:
import { Injectable } from '@angular/core';
import { HeroServiceClient } from './proto/hero/hero_pb_service';
import { HeroById, Hero, HeroList } from './proto/hero/hero_pb';@Injectable({
providedIn: 'root'
})
export class ApiService {
client: HeroServiceClient; constructor() {
this.client = new HeroServiceClient('http://localhost:3001');
} getHero(path, val): Promise <Hero> {
return new Promise((resolve, reject) => {
const req = new HeroById();
req.setId(val);
this.client.getHeroById(req, null, (err, response: Hero) => {
if (err) {
return reject(err);
}
resolve(response.toObject());
});
});
}
}
Adding a proxy for gRPC requests
When you run the Angular app, you’ll notice the Angular Service cannot connect to the gRPC endpoint, even though the address is correct:
Going back to earlier, you’ll remember that browsers don’t have full support for gRPC yet, so the gRPC-web library is sending a POST request instead:
The @improbable-eng/grpc-web client uses multiple techniques to efficiently invoke gRPC services. Most modern browsers support the Fetch API, which allows for efficient reading of partial, binary responses. For older browsers, it automatically falls back to XMLHttpRequest.
Until there is better cross-browser support for gRPC, we can use Envoy Proxy to pass through requests. We start by adding an envoy.yaml to configure our proxy settings. Note: you may need to change the hosts target port_value from 5000 depending on your NestJS server settings:
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 8081 }static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: echo_service
max_grpc_timeout: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: echo_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
hosts: [{ socket_address: { address: localhost, port_value: 5000 }}]
We can now run Envoy Proxy locally using a Docker command:
docker run -d -p 8080:8080 -p 8081:8081 -t envoyproxy/envoy-alpine:v1.14.1 -v ./envoy.yaml:/etc/envoy/envoy.yaml
If successful you should see the proxy running in your terminal:
Now we can point our Angular client-side requests to Envoy Proxy at port 8080, and it will forward them onto our NestJS server at port 5000!
this.client = new HeroServiceClient('http://localhost:8080');
Running Angular again, you should see requests get sent to the Envoy Proxy on port 8080, and returning with gRPC data!
Summary
There are some pros and cons to implementing gRPC for client-side applications. The main negative points are:
- Use of a proxy is not ideal, and adds overhead & complexity.
- Requires addition gRPC-web libraries for compatibility.
- If you have multiple auto-generated client-side services, they contain repeated code, adding bloat to your application.
- Takes time for a team to skill-up and support a new data format.
The positives of gRPC for client-side applications:
- Proto buffers allow a single-source of truth for models, which are language-agnostic.
- We can auto-generate our Types and Services, saving time from keeping everything up-to-date manually.
- Even the fallback fetch requests allow more efficient reading of partial, binary responses vs a standard REST API, and using gRPC-web is easier than implementing your own fetch binary requests manually.
- In the future as browser support improves, the amount of polyfill and generated code should be reduced.
Hope that helps you get started with gRPC and TypeScript!
You can check out my full demo with source code here:
https://github.com/kmturley/angular-nest-grpc