Guide to integrating NextJS with gRPC and NestJS via HTTP/2

Introduction

  • gRPC is a high-performance RPC (Remote Procedure Call) framework developed by Google, using Protocol Buffers (protobuf) as the interface definition language and data serialization format. Unlike traditional REST which uses text (JSON), gRPC transmits data in binary format to optimize payload size and processing speed. In particular, gRPC operates based on HTTP/2, providing outstanding advantages such as: Multi-plexing (sending multiple concurrent requests over a single connection), Header Compression, and Server Push, helping to reduce latency and increase bandwidth for microservices systems.
  • In this article, I will use NextJS for the frontend and act as a BFF (Backend For Frontend) as a proxy server to aggregate information and communicate via HTTP/2 gRPC with the NestJS server.
  • Our connection will be as follows: Frontend connects to NextJS proxy server using restful api HTTP/1.1, NextJS connects to NestJS using gRPC via HTTP/2, we need a NextJS middleware to handle it, not direct connection from frontend to NestJS using gRPC, to avoid returning proto config file information to the frontend side, enhancing security, hiding server information and simplifying handling for the frontend side.

Prerequisites

First, you need some basic knowledge about NextJS and NestJS including project creation, please see previous articles to know how to create NextJS and NestJS projects before continuing.


Detail

General Setup

The following config information is related to the proto file used for gRPC communication between NextJS proxy and NestJS server, please do the same for both projects including the following steps (in reality you could create a sub repository to share proto file between projects):


First, install the following packages

brew install bufbuild/buf/buf
yarn add -D @bufbuild/buf @bufbuild/protoc-gen-es @connectrpc/protoc-gen-connect-es
yarn add @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf

  • bufbuild/buf/buf: This is a command-line tool (CLI) that serves as a modern replacement for the traditional protoc compiler. It helps you manage .proto files, perform linting, detect breaking changes (to ensure backward compatibility), and efficiently handle code generation.
  • Code Generation (Development Dependencies): these are used only during development to transform .proto files into .ts or .js files.
    • @bufbuild/buf: The Node.js distribution of the Buf CLI, allowing you to run Buf commands directly via npx or scripts within your package.json.
    • @bufbuild/protoc-gen-es: A plugin used to generate data structures (Messages) and metadata from Protobuf files into ECMAScript (TypeScript/JavaScript).
    • @connectrpc/protoc-gen-connect-es: A plugin used to generate service interfaces for both clients and servers based on the Connect protocol.
  • Runtime Libraries (Dependencies): these libraries run directly within your application code.
    • @connectrpc/connect: The core library that enables you to write request handlers for services and invoke APIs (Clients). It supports the gRPC, gRPC-Web, and Connect protocols.
    • @connectrpc/connect-node: Provides specific adapters to run Connect within the Node.js environment (supporting both HTTP and HTTP/2).
    • @bufbuild/protobuf: The runtime library responsible for handling the serialization and deserialization of Protobuf objects in JavaScript/TypeScript.


Create file proto/aggregator.proto

syntax = "proto3";
package aggregator.v1;

service AggregatorService {
rpc GetPrice (IdRequest) returns (PriceResponse);
rpc GetNews (IdRequest) returns (NewsResponse);
rpc GetSocial (IdRequest) returns (SocialResponse);
}

message IdRequest { string id = 1; }
message PriceResponse { double price = 1; double change = 2; }
message NewsResponse { repeated string titles = 1; }
message SocialResponse { int32 followers = 1; }

The above code defines the data structure and gRPC services using Protocol Buffers. It defines AggregatorService with 3 methods (RPC): GetPrice, GetNews, and GetSocial. Each method specifies the input data type (IdRequest) and corresponding output, helping to ensure consistency in data types between Client (NextJS) and Server (NestJS).


Create file buf.gen.yaml

version: v1
plugins:
- plugin: es
out: gen
opt: target=ts
- plugin: connect-es
out: gen
opt: target=ts

This file is the configuration for the buf tool to automatically generate TypeScript source code from the .proto file. The es plugin is responsible for creating message definitions (messages), while the connect-es plugin creates service and client definitions to be able to call gRPC methods easily in a TypeScript environment.


Add the following command to the package.json file

{
"scripts": {
"generate": "buf generate proto"
}
}


After executing the command, files will be created in the gen folder as follows:


NestJS Project

Install the following packages

yarn add @nestjs/platform-fastify

Since NestJS uses Express by default which does not support HTTP/2, we have to switch to using Fastify which supports HTTP/2.


Create file service/coin.service.ts

import { Injectable } from "@nestjs/common";

@Injectable()
export class CoinService {
private generateSeed(id: string): number {
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = id.charCodeAt(i) + ((hash << 5) - hash);
}
return Math.abs(hash);
}

async getPrice(id: string) {
const seed = this.generateSeed(id);
const price = (seed % 99000) + 1000 + Math.random();
const change = (Math.random() * 20 - 10).toFixed(2);
return {
id,
price: parseFloat(price.toFixed(2)),
change: parseFloat(change),
currency: "USD",
};
}

async getNews(id: string) {
const newsDatabase = [
`${id.toUpperCase()} Adoption Rises in Asia`,
`New Regulatory Framework for ${id.toUpperCase()}`,
`Why ${id.toUpperCase()} is Dumping Today`,
`Whale Alert: Huge movement in ${id.toUpperCase()} wallets`,
`${id.toUpperCase()} Network Upgrade Successful`,
`Institutional Investors are buying ${id.toUpperCase()}`,
];
const shuffled = newsDatabase.sort(() => 0.5 - Math.random());
const titles = shuffled.slice(0, Math.floor(Math.random() * 2) + 2);
return {
id,
titles,
timestamp: new Date().toISOString(),
};
}

async getSocial(id: string) {
const seed = this.generateSeed(id);
const followers = (seed % 1000000) + Math.floor(Math.random() * 50000);
return {
id,
platform: "X (Twitter)",
followers,
sentiment: Math.random() > 0.5 ? "Bullish" : "Bearish",
};
}
}

This is where the main business logic is handled on the Backend side. It contains methods for calculating simulated prices, getting random news, and social media metrics based on the coin code (ID).


Create file router/coin.routes.ts

import { ConnectRouter } from "@connectrpc/connect";
import { AggregatorService } from "gen/aggregator_pb";
import type { CoinService } from "src/service/coin.service";

export const coinRoutes = (router: ConnectRouter) => {
return (serviceInstance: CoinService) => {
router.service(AggregatorService, {
getPrice: req => serviceInstance.getPrice(req.id),
getNews: req => serviceInstance.getNews(req.id),
getSocial: req => serviceInstance.getSocial(req.id),
});
};
};

This code performs mapping between gRPC service definition (AggregatorService) with actual logic in CoinService. It registers RPC methods with ConnectRouter, specifying that when a gRPC request arrives, it will call the corresponding function in CoinService to return results to the client.


Create file router/grpc.router.ts

import { connectNodeAdapter } from "@connectrpc/connect-node";
import type { NestFastifyApplication } from "@nestjs/platform-fastify";
import { CoinService } from "src/service/coin.service";
import { coinRoutes } from "./coin.routes";

export const grpcRouter = (app: NestFastifyApplication) => {
const coinService = app.get(CoinService);
const adapter = connectNodeAdapter({
routes: router => {
coinRoutes(router)(coinService);
},
});

app.use((req, res, next) => {
const isGrpcRequest = req.url.startsWith("/aggregator.v1.");
if (isGrpcRequest) {
console.log(`[gRPC Request] Path: ${req.url} - Protocol: HTTP/${req.httpVersion}`);
return adapter(req, res);
}
next();
});
};

This file sets up a middleware for NestJS using connectNodeAdapter. Its task is to check incoming requests; if the request has a path starting with the gRPC namespace (/aggregator.v1.), it will redirect that request to the gRPC handler for processing. Otherwise, it will ignore it so that traditional NestJS REST routes can receive it.


Update file main.ts

import { NestFactory } from "@nestjs/core";
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
import { AppModule } from "./app.module";
import { grpcRouter } from "./router/grpc.router";

async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
http2: true,
}),
);
grpcRouter(app);
await app.listen(4000);
}
bootstrap();

This is the NestJS application initialization file. The important point is the use of FastifyAdapter with configuration HTTP/2: true, allowing the server to support the HTTP/2 protocol. It then calls grpcRouter(app) to register the previously configured gRPC routes into the application lifecycle.


Update file app.module.ts

import { Module } from "@nestjs/common";
import { CoinService } from "./service/coin.service";

@Module({
providers: [CoinService],
})
export class AppModule {}


NextJS Project

Create .env file

SERVER_URL = http://localhost:4000


Create file app/_lib/grpc-client.ts

import { AggregatorService } from "@/gen/aggregator_pb";
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-node";

export const grpcClient = createClient(
AggregatorService,
createConnectTransport({
baseUrl: process.env.SERVER_URL || '',
httpVersion: "2",
}),
);

This code snippet initializes a gRPC Client on the NextJS side. It uses createConnectTransport to establish a connection to the NestJS Server (running on port 4000) via the HTTP/2 protocol. grpcClient then provides type-safe functions to call Backend like a normal local function.


Create file app/api/coin-summary/[id]/route.ts

import { grpcClient } from "@/app/\_lib/grpc-client";

export async function GET(req: Request, { params }: Props) {
const { id: coinId } = await params;
const [priceRes, newsRes, socialRes] = await Promise.allSettled([grpcClient.getPrice({ id: coinId }), grpcClient.getNews({ id: coinId }), grpcClient.getSocial({ id: coinId })]);

const responseData = {
id: coinId,
price: priceRes.status === "fulfilled" ? priceRes.value : null,
news: newsRes.status === "fulfilled" ? newsRes.value.titles : [],
social: socialRes.status === "fulfilled" ? socialRes.value : { followers: 0 },
health: {
price: priceRes.status === "fulfilled",
news: newsRes.status === "fulfilled",
social: socialRes.status === "fulfilled",
},
};
return Response.json(responseData);
}

type Props = {
params: Promise<{ id: string }>;
};

This is an API Route in NextJS acting as a BFF. It receives requests from the Frontend, then performs concurrent calls to 3 different gRPC services via Promise.allSettled. This technique helps aggregate data from multiple sources quickly and safely: if one service fails or times out, the rest can still return data to the user instead of breaking the entire request.


Create file app/coin/page.tsx

"use client";

import { Alert, Card, Divider, Select, Space, Spin, Typography } from "antd";
import { useEffect, useState } from "react";

const { Title, Text } = Typography;

interface PriceData {
price: number;
change: number;
}

interface CoinSummary {
id: string;
price?: PriceData;
news: string[];
errors?: {
price?: boolean;
};
}

interface CoinOption {
value: string;
label: string;
}

export default function CoinPage() {
const [selectedCoin, setSelectedCoin] = useState<string>("btc");
const [data, setData] = useState<CoinSummary | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const coinOptions: CoinOption[] = [
{ value: "btc", label: "Bitcoin" },
{ value: "eth", label: "Ethereum" },
{ value: "sol", label: "Solana" },
{ value: "ada", label: "Cardano" },
];
const fetchCoinData = async (coinId: string) => {
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/coin-summary/${coinId}`);
if (!res.ok) throw new Error("Failed to fetch data");

const result: CoinSummary = await res.json();
setData(result);
} catch (err) {
setError("Unable to load data. Please try again later.");
console.error(err);
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchCoinData(selectedCoin);
}, [selectedCoin]);

return (
<div className="max-w-2xl mx-auto my-10 px-5">
<Title level={2} className="mb-6 text-center">
Crypto Dashboard
</Title>
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
<div className="mb-2">
<Text strong className="text-gray-600">
Select Asset:
</Text>
</div>
<Select size="large" className="w-full" value={selectedCoin} options={coinOptions} onChange={value => setSelectedCoin(value)} />
</div>
<Divider />
{loading ? (
<div className="flex flex-col items-center justify-center py-20">
<Spin size="large" description="Loading asset data..." />
</div>
) : error ? (
<Alert title={error} type="error" showIcon className="shadow-sm" />
) : data ? (
<Space orientation="vertical" size="middle" className="w-full">
<div className="flex justify-between items-center">
<Title level={3} className="uppercase m-0">
{data.id} <span className="text-gray-400 font-light text-xl">Overview</span>
</Title>
</div>
<Card title={<span className="text-gray-500 font-medium uppercase text-xs tracking-wider">Price Statistics</span>} hoverable className="shadow-sm border-none bg-white">
{data.errors?.price ? (
<Alert title="Price service is currently unavailable." type="warning" />
) : (
<div className="flex items-baseline">
<Text className="text-4xl font-bold tracking-tight">${data.price?.price?.toLocaleString()}</Text>
<div className={`ml-3 px-2 py-0.5 rounded ${data.price?.change && data.price.change >= 0 ? "bg-green-50" : "bg-red-50"}`}>
<Text type={data.price?.change && data.price.change >= 0 ? "success" : "danger"} className="text-sm font-semibold">
{data.price?.change && data.price.change >= 0 ? "+" : ""}
{data.price?.change}%
</Text>
</div>
</div>
)}
</Card>
<Card title={<span className="text-gray-500 font-medium uppercase text-xs tracking-wider">Market Sentiment</span>} hoverable className="shadow-sm border-none bg-white">
{data.news && data.news.length > 0 ? (
<ul className="list-none p-0 m-0 space-y-3">
{data.news.map((item, index) => (
<li key={index} className="flex items-start">
<span className="text-blue-500 mr-2"></span>
<Text className="text-gray-600 leading-relaxed">{item}</Text>
</li>
))}
</ul>
) : (
<Text italic className="text-gray-400 block text-center py-4">
No recent news found for this asset.
</Text>
)}
</Card>
</Space>
) : null}
</div>
);
}

This Dashboard page allows selecting a cryptocurrency from a list. When users change their selection, the page calls NextJS's internal API, receives aggregated data from gRPC, and visually displays price information and market news.


Result as follows


When checking NestJS server logs, you will see the protocol is HTTP/2

[gRPC Request] Path: /aggregator.v1.AggregatorService/GetPrice - Protocol: HTTP/2.0
[gRPC Request] Path: /aggregator.v1.AggregatorService/GetNews - Protocol: HTTP/2.0
[gRPC Request] Path: /aggregator.v1.AggregatorService/GetSocial - Protocol: HTTP/2.0


Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Sitemap

NodeJS Practice Series