backend

gRPC in Node.js and TypeScript: Production Guide (2026)

June 14, 2026

gRPC in Node.js and TypeScript: Production Guide (2026)

Build a production gRPC service in Node.js and TypeScript using @grpc/grpc-js and @grpc/proto-loader: define a .proto, generate typed stubs, implement unary and server-streaming RPCs, set deadlines, return proper status codes, and expose the standard health-checking protocol.

TL;DR

This hands-on guide builds a fully typed gRPC service in Node.js and TypeScript end to end. You define an InventoryService in a .proto file, generate TypeScript stubs with proto-loader-gen-types, then implement a unary RPC, a server-streaming RPC, and a unary RPC that fails with proper gRPC status codes. You will set client deadlines and watch them surface as DEADLINE_EXCEEDED, return rich error metadata in trailers, and wire up the standard grpc.health.v1.Health checking protocol plus server reflection. Stack: @grpc/grpc-js 1.14.41, @grpc/proto-loader 0.8.12, grpc-health-check 2.1.03, and @grpc/reflection 1.0.44 on TypeScript 6.0.35. Every file was type-checked under strict TypeScript and run end to end on 14 June 2026 — a six-test node:test suite passes. Budget about 45–60 minutes.

What you'll learn

  • Why to use @grpc/grpc-js + @grpc/proto-loader instead of the deprecated grpc C-package
  • How to define a service and messages in a proto3 .proto file
  • How to generate TypeScript types from a .proto file with proto-loader-gen-types
  • How to implement a typed unary RPC and a server-streaming RPC
  • How gRPC error handling works in Node.js: status codes and metadata trailers
  • How to set a deadline on a gRPC call and handle DEADLINE_EXCEEDED
  • How to add the standard health-checking protocol and server reflection
  • How to verify the whole thing with an in-process node:test suite

Prerequisites

  • Node.js 24 (codename "Krypton," Active LTS, supported through 30 April 2028)6 or Node 22 Maintenance LTS. Both run TypeScript directly via tsx, so there is no build step in dev.
  • Comfort with async/await and basic TypeScript. No prior gRPC or Protocol Buffers experience required.
  • A terminal and a code editor. No Docker, no database, no cloud account — the server and client run locally.

Versions are pinned throughout. Pasting latest into a tutorial is how a working example rots three weeks later.

Why @grpc/grpc-js, not the old grpc package

If you search for "grpc node," plenty of older results still import a package literally named grpc. That is the original C-based addon binding, and the gRPC team deprecated it in April 2021; it no longer receives updates, and the official recommendation is to use @grpc/grpc-js instead7. @grpc/grpc-js is a pure-JavaScript implementation — no native build, no node-gyp, no prebuilt-binary roulette on a new Node release. Paired with @grpc/proto-loader, it loads your .proto at runtime and gives you a clean, typed surface. That is the combination this guide uses, and the combination you should reach for in 2026.

gRPC itself is a contract-first RPC framework: you describe your service in a .proto file, and both client and server are generated from that single source of truth. Calls travel over HTTP/2 with Protocol Buffers on the wire, which is why gRPC is a popular choice for internal service-to-service traffic where REST's text framing and ad-hoc schemas become a liability.

Step 1 — Scaffold the project

Create a project and install the runtime and dev dependencies, all pinned:

mkdir grpc-inventory && cd grpc-inventory
npm init -y
npm pkg set type=module

# runtime
npm install @grpc/grpc-js@1.14.4 @grpc/proto-loader@0.8.1 \
  grpc-health-check@2.1.0 @grpc/reflection@1.0.4

# dev
npm install -D typescript@6.0.3 tsx@4.22.4 @types/node@25.9.3

tsx runs .ts files directly, so the development loop is tsx src/server.ts with no compile step. Add a tsconfig.json tuned for strictness. The one non-obvious choice is "moduleResolution": "Bundler" — the code that proto-loader-gen-types generates uses extensionless relative imports, and Bundler resolution handles those cleanly while still type-checking under verbatimModuleSyntax:

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"]
}

Then create the directory layout the rest of the guide uses:

mkdir -p proto/inventory/v1 src/gen src/server src/client

Step 2 — Define the service in a .proto file

The .proto file is the contract. Create proto/inventory/v1/inventory.proto:

syntax = "proto3";

package inventory.v1;

// InventoryService manages product stock for a small store.
service InventoryService {
  // Unary RPC: fetch a single product by id.
  rpc GetProduct(GetProductRequest) returns (Product);

  // Server-streaming RPC: emit a bounded sequence of stock updates.
  rpc WatchStock(WatchStockRequest) returns (stream StockUpdate);

  // Unary RPC that can fail: reserve units of a product.
  rpc ReserveStock(ReserveStockRequest) returns (ReserveStockResponse);
}

message GetProductRequest {
  string id = 1;
}

message Product {
  string id = 1;
  string name = 2;
  int32 stock = 3;
  int64 price_cents = 4;
}

message WatchStockRequest {
  string id = 1;
  int32 updates = 2; // number of updates to emit before completing
}

message StockUpdate {
  string id = 1;
  int32 stock = 2;
  int64 ts_unix_ms = 3;
}

message ReserveStockRequest {
  string id = 1;
  int32 quantity = 2;
}

message ReserveStockResponse {
  string reservation_id = 1;
  int32 remaining = 2;
}

Two design notes worth internalizing. First, the stream keyword on WatchStock's return type is what makes it a server-streaming RPC — the server sends many StockUpdate messages for one request. Second, price_cents is an int64. Protocol Buffers can carry a full 64-bit integer, but JavaScript's number cannot represent integers beyond 2^53 without losing precision, so the official proto3-to-JSON mapping renders 64-bit fields as strings8. You will see that play out in the generated types in a moment.

Step 3 — Generate TypeScript types from the proto file

@grpc/proto-loader ships a proto-loader-gen-types CLI that turns a .proto into TypeScript declaration files. Add it as a script in package.json:

{
  "scripts": {
    "gen": "proto-loader-gen-types --longs=String --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js -I proto --outDir=src/gen inventory/v1/inventory.proto",
    "typecheck": "tsc --noEmit",
    "server": "tsx src/server/index.ts",
    "client": "tsx src/client/index.ts",
    "test": "node --import tsx --test src/server/*.test.ts"
  }
}

Run it:

npm run gen

The flags matter. --longs=String is what turns those int64 fields into TypeScript strings (consistent with the JSON mapping). --enums=String, --defaults, and --oneofs control how enums, omitted fields, and oneof groups are typed. The -I proto include directory plus the inventory/v1/inventory.proto path (relative to that directory) is the invocation that resolves imports without a warning — passing the full path without a matching -I prints a confusing "not found in any of the include paths" message.

proto-loader-gen-types writes a tree under src/gen. The entry point is src/gen/inventory.ts, which exports a ProtoGrpcType describing your whole package:

export interface ProtoGrpcType {
  inventory: {
    v1: {
      GetProductRequest: MessageTypeDefinition<...>
      InventoryService: SubtypeConstructor<typeof grpc.Client, _inventory_v1_InventoryServiceClient> & { service: _inventory_v1_InventoryServiceDefinition }
      Product: MessageTypeDefinition<...>
      // ...one entry per message
    }
  }
}

Alongside it, src/gen/inventory/v1/InventoryService.ts exports three types you will use directly: InventoryServiceClient (the typed client), InventoryServiceHandlers (the shape your server must implement), and InventoryServiceDefinition.

The camelCase gotcha. Because we did not pass --keepCase, proto-loader converts snake_case proto fields to camelCase in TypeScript. So price_cents becomes priceCents, reservation_id becomes reservationId, and ts_unix_ms becomes tsUnixMs. If you write the snake_case name in your handler, the strict compiler rejects it — which is exactly the safety you generated these types for.

Step 4 — Load the proto with a typed package definition

Generated types describe the shape; you still load the actual service descriptor at runtime. Centralize that in src/proto.ts so the server and client share one loader:

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import type { ProtoGrpcType } from './gen/inventory';

const here = dirname(fileURLToPath(import.meta.url));
const PROTO_DIR = join(here, '..', 'proto');
const PROTO_PATH = join(PROTO_DIR, 'inventory', 'v1', 'inventory.proto');

// These options MUST match the flags passed to proto-loader-gen-types,
// or the runtime objects won't match the generated types.
export const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
  includeDirs: [PROTO_DIR],
});

export const proto = grpc.loadPackageDefinition(
  packageDefinition,
) as unknown as ProtoGrpcType;

A common source of "the types say one thing but runtime does another" bugs is a mismatch between these loadSync options and the generator flags. Keep them aligned: longs: String here pairs with --longs=String there, and so on. The as unknown as ProtoGrpcType cast is the documented bridge between the untyped loadPackageDefinition return value and your generated interface.

Step 5 — Implement the server

Now the service. Create src/server/index.ts. Start with an in-memory store and a mapper that produces a Product message (note priceCents as a string):

import * as grpc from '@grpc/grpc-js';
import { fileURLToPath } from 'node:url';
import { HealthImplementation } from 'grpc-health-check';
import { ReflectionService } from '@grpc/reflection';
import { proto, packageDefinition } from '../proto';
import type { InventoryServiceHandlers } from '../gen/inventory/v1/InventoryService';
import type { Product } from '../gen/inventory/v1/Product';

interface Row { id: string; name: string; stock: number; priceCents: bigint }

const store = new Map<string, Row>([
  ['sku-1', { id: 'sku-1', name: 'Mechanical Keyboard', stock: 12, priceCents: 8999n }],
  ['sku-2', { id: 'sku-2', name: 'USB-C Cable', stock: 0, priceCents: 1299n }],
]);

const toProduct = (r: Row): Product => ({
  id: r.id,
  name: r.name,
  stock: r.stock,
  priceCents: r.priceCents.toString(),
});

Typed handlers and the error model

The handlers object is typed as InventoryServiceHandlers, so the compiler verifies every method signature against the proto. A unary handler receives (call, callback); you signal success with callback(null, response) and an error with callback({ code, message }) using a grpc.status code:

const handlers: InventoryServiceHandlers = {
  GetProduct(call, callback) {
    const row = store.get(call.request.id);
    if (!row) {
      callback({ code: grpc.status.NOT_FOUND, message: `product ${call.request.id} not found` });
      return;
    }
    callback(null, toProduct(row));
  },

  ReserveStock(call, callback) {
    const { id, quantity } = call.request;
    if (quantity <= 0) {
      const trailers = new grpc.Metadata();
      trailers.set('field-violation', 'quantity');
      callback({ code: grpc.status.INVALID_ARGUMENT, message: 'quantity must be > 0', metadata: trailers });
      return;
    }
    const row = store.get(id);
    if (!row) {
      callback({ code: grpc.status.NOT_FOUND, message: `product ${id} not found` });
      return;
    }
    if (row.stock < quantity) {
      callback({ code: grpc.status.FAILED_PRECONDITION, message: `only ${row.stock} unit(s) available` });
      return;
    }
    row.stock -= quantity;
    callback(null, { reservationId: `r_${Date.now()}`, remaining: row.stock });
  },

  WatchStock(call) {
    const { id, updates } = call.request;
    const row = store.get(id);
    if (!row) {
      call.emit('error', { code: grpc.status.NOT_FOUND, message: `product ${id} not found` });
      return;
    }
    let sent = 0;
    const tick = () => {
      if (call.cancelled || sent >= updates) {
        call.end();
        return;
      }
      sent += 1;
      call.write({ id: row.id, stock: row.stock, tsUnixMs: Date.now().toString() });
      setTimeout(tick, 150);
    };
    tick();
  },
};

This is the heart of gRPC error handling in Node.js. Instead of HTTP status codes, gRPC has its own canonical set: NOT_FOUND (5), INVALID_ARGUMENT (3), FAILED_PRECONDITION (9), UNAVAILABLE (14), DEADLINE_EXCEEDED (4), and the rest9. Map your domain failures onto them deliberately: a missing product is NOT_FOUND, a bad input is INVALID_ARGUMENT, and "you asked for 5 but only 0 are in stock" is FAILED_PRECONDITION. When you need to send structured detail back, attach a grpc.Metadata object — those key/value pairs ride along in the call's trailers10, and the client can read them off the error.

The streaming handler is different. WatchStock receives only call, a writable stream. You push messages with call.write(...) and finish the stream with call.end(). To fail a stream you emit an error event carrying a status. Crucially, you check call.cancelled before each write: if the client hangs up or its deadline expires, you stop doing work instead of writing into a dead connection.

Wire up health checks and reflection

Two things separate a toy server from one you can actually operate: a health endpoint and reflection. Both are standard gRPC services you mount onto your server. grpc-health-check implements the grpc.health.v1.Health protocol — the same one Kubernetes grpc liveness probes speak11:

export function buildServer(): grpc.Server {
  const server = new grpc.Server();
  server.addService(proto.inventory.v1.InventoryService.service, handlers);

  // Standard health checking protocol (grpc.health.v1.Health).
  const health = new HealthImplementation({ 'inventory.v1.InventoryService': 'SERVING' });
  health.addToServer(server);

  // Server reflection so grpcurl / Postman can discover the schema at runtime.
  const reflection = new ReflectionService(packageDefinition);
  reflection.addToServer(server);

  return server;
}

HealthImplementation takes an initial status map keyed by service name, where 'SERVING', 'NOT_SERVING', and 'UNKNOWN' are the legal states; call health.setStatus(name, 'NOT_SERVING') when a dependency goes down so orchestrators stop routing to you. ReflectionService takes the same packageDefinition you already loaded, and once mounted, tools like grpcurl can list and call your methods without you shipping them the .proto.

Start the server

Finally, bind and start. In @grpc/grpc-js, bindAsync starts the server for you — the old server.start() call is marked @deprecated ("No longer needed as of version 1.10.x") and calling it now only prints a warning12:

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const server = buildServer();
  const addr = process.env.ADDR ?? '127.0.0.1:50051';
  server.bindAsync(addr, grpc.ServerCredentials.createInsecure(), (err, port) => {
    if (err) { console.error(err); process.exit(1); }
    console.log(`InventoryService listening on ${addr} (port ${port})`);
  });
  const shutdown = () => server.tryShutdown(() => process.exit(0));
  process.on('SIGTERM', shutdown);
  process.on('SIGINT', shutdown);
}

createInsecure() is fine on localhost; in production you would pass real TLS credentials. The tryShutdown handlers give in-flight calls a chance to finish on SIGTERM instead of dropping them — the same graceful-drain discipline you would apply to any long-lived Node process. Launch it:

npm run server
# InventoryService listening on 127.0.0.1:50051 (port 50051)

Step 6 — Write a typed client with a deadline

Create src/client/index.ts. The generated InventoryServiceClient constructor lives on proto.inventory.v1.InventoryService. Wrapping the callback-style unary call in a Promise gives you async/await, and the place to set a deadline is the CallOptions argument:

import * as grpc from '@grpc/grpc-js';
import { proto } from '../proto';
import type { InventoryServiceClient } from '../gen/inventory/v1/InventoryService';
import type { Product__Output } from '../gen/inventory/v1/Product';

const ADDR = process.env.ADDR ?? '127.0.0.1:50051';

export function makeClient(addr = ADDR): InventoryServiceClient {
  return new proto.inventory.v1.InventoryService(
    addr,
    grpc.credentials.createInsecure(),
  );
}

// Promise wrapper around the unary GetProduct call, with a deadline.
function getProduct(
  client: InventoryServiceClient,
  id: string,
  deadlineMs = 2000,
): Promise<Product__Output> {
  const options: grpc.CallOptions = { deadline: new Date(Date.now() + deadlineMs) };
  return new Promise((resolve, reject) => {
    client.GetProduct({ id }, options, (err, res) => {
      if (err || !res) { reject(err); return; }
      resolve(res);
    });
  });
}

A gRPC deadline is an absolute point in time, not a duration. You compute it as new Date(Date.now() + ms) and pass it as options.deadline. gRPC propagates that deadline across the call; if the server has not responded by then, the call fails locally with status DEADLINE_EXCEEDED (4). This is one of the most important reliability knobs in gRPC — always set a deadline, because by default a call has none and will wait indefinitely.

Now exercise all three RPCs. The streaming call returns a readable stream you consume with data, error, and end events:

async function main(): Promise<void> {
  const client = makeClient();

  const product = await getProduct(client, 'sku-1');
  console.log('GetProduct:', product);

  await new Promise<void>((resolve) => {
    const stream = client.WatchStock({ id: 'sku-1', updates: 3 });
    stream.on('data', (u) => console.log('StockUpdate:', u));
    stream.on('error', (e: grpc.ServiceError) => { console.error('stream error', e.code); resolve(); });
    stream.on('end', () => resolve());
  });

  client.ReserveStock({ id: 'sku-2', quantity: 5 }, (err, res) => {
    if (err) console.error('ReserveStock failed:', grpc.status[err.code], err.details);
    else console.log('ReserveStock:', res);
    client.close();
  });
}

main().catch((e) => { console.error(e); process.exit(1); });

With the server running in one terminal, run the client in another:

npm run client
GetProduct: {
  id: 'sku-1',
  name: 'Mechanical Keyboard',
  stock: 12,
  priceCents: '8999'
}
StockUpdate: { id: 'sku-1', stock: 12, tsUnixMs: '1781429057452' }
StockUpdate: { id: 'sku-1', stock: 12, tsUnixMs: '1781429057607' }
StockUpdate: { id: 'sku-1', stock: 12, tsUnixMs: '1781429057762' }
ReserveStock failed: FAILED_PRECONDITION only 0 unit(s) available

priceCents arrives as the string '8999' (the int64-to-string mapping at work), the stream delivers exactly three updates, and the over-reservation against the out-of-stock sku-2 comes back as FAILED_PRECONDITION. grpc.status[err.code] turns the numeric code back into its readable name for logs; err.details is the bare server message, while err.message additionally prefixes the numeric code and status name.

Verification

Hand-running the client proves the happy paths; an automated suite proves the failure paths and pins the behavior so a refactor cannot silently break it. The trick is to bind the server to 127.0.0.1:0 — port zero asks the OS for a free port — so the test is hermetic and never collides with a running server. Create src/server/index.test.ts:

import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import * as grpc from '@grpc/grpc-js';
import { buildServer } from './index';
import { proto } from '../proto';
import type { InventoryServiceClient } from '../gen/inventory/v1/InventoryService';

let server: grpc.Server;
let client: InventoryServiceClient;

before(async () => {
  server = buildServer();
  const port = await new Promise<number>((resolve, reject) => {
    server.bindAsync('127.0.0.1:0', grpc.ServerCredentials.createInsecure(), (err, p) => {
      if (err) reject(err); else resolve(p);
    });
  });
  client = new proto.inventory.v1.InventoryService(`127.0.0.1:${port}`, grpc.credentials.createInsecure());
});

after(() => { client.close(); server.forceShutdown(); });

test('unary GetProduct returns a typed product', async () => {
  const res = await new Promise<any>((resolve, reject) =>
    client.GetProduct({ id: 'sku-1' }, (e, r) => (e ? reject(e) : resolve(r))));
  assert.equal(res.name, 'Mechanical Keyboard');
  assert.equal(res.stock, 12);
  assert.equal(res.priceCents, '8999'); // int64 -> string via longs=String
});

test('GetProduct maps a missing id to NOT_FOUND', async () => {
  const err = await new Promise<grpc.ServiceError>((resolve) =>
    client.GetProduct({ id: 'nope' }, (e) => resolve(e as grpc.ServiceError)));
  assert.equal(err.code, grpc.status.NOT_FOUND);
});

test('ReserveStock returns FAILED_PRECONDITION when stock is insufficient', async () => {
  const err = await new Promise<grpc.ServiceError>((resolve) =>
    client.ReserveStock({ id: 'sku-2', quantity: 5 }, (e) => resolve(e as grpc.ServiceError)));
  assert.equal(err.code, grpc.status.FAILED_PRECONDITION);
});

test('ReserveStock returns INVALID_ARGUMENT with a trailer', async () => {
  const err = await new Promise<grpc.ServiceError>((resolve) =>
    client.ReserveStock({ id: 'sku-1', quantity: 0 }, (e) => resolve(e as grpc.ServiceError)));
  assert.equal(err.code, grpc.status.INVALID_ARGUMENT);
  assert.equal(err.metadata?.get('field-violation')[0], 'quantity');
});

test('server-streaming WatchStock emits the requested number of updates', async () => {
  const updates: any[] = [];
  await new Promise<void>((resolve, reject) => {
    const stream = client.WatchStock({ id: 'sku-1', updates: 3 });
    stream.on('data', (u) => updates.push(u));
    stream.on('end', resolve);
    stream.on('error', reject);
  });
  assert.equal(updates.length, 3);
  assert.equal(updates[0].id, 'sku-1');
});

test('a tight deadline surfaces DEADLINE_EXCEEDED', async () => {
  const err = await new Promise<grpc.ServiceError | null>((resolve) => {
    const stream = client.WatchStock(
      { id: 'sku-1', updates: 10 },
      { deadline: new Date(Date.now() + 120) },
    );
    stream.on('data', () => {});
    stream.on('error', (e) => resolve(e as grpc.ServiceError));
    stream.on('end', () => resolve(null));
  });
  assert.ok(err, 'expected an error');
  assert.equal(err!.code, grpc.status.DEADLINE_EXCEEDED);
});

Type-check, then run:

npm run typecheck   # tsc --noEmit, exits 0
npm test
# tests 6
# pass 6
# fail 0

Six green tests confirm the unary path, the three error mappings, the trailer metadata, the streaming count, and the deadline. To eyeball the health endpoint and reflection without writing client code, install grpcurl and point it at the running server:

grpcurl -plaintext 127.0.0.1:50051 list
grpcurl -plaintext 127.0.0.1:50051 grpc.health.v1.Health/Check

The first command works only because reflection is mounted; the second returns { "status": "SERVING" }.

Troubleshooting

These are the failures you are most likely to hit, with the fix for each.

  • 14 UNAVAILABLE: No connection established. The client could not reach the server. Confirm the server is actually listening (the bind callback logged a port), that the address matches exactly, and that you used grpc.credentials.createInsecure() on the client to match ServerCredentials.createInsecure() on the server. A TLS/plaintext mismatch shows up as UNAVAILABLE, not as a clear handshake error.
  • proto/...proto not found in any of the include paths. You passed the full proto path to proto-loader-gen-types without a matching -I. Pass the include directory with -I proto and the proto path relative to it (inventory/v1/inventory.proto), as in Step 3.
  • "Object literal may only specify known properties... did you mean priceCents?" You wrote a snake_case field name. Without --keepCase, proto-loader camel-cases fields. Use priceCents, reservationId, tsUnixMs — or regenerate with --keepCase if you prefer snake_case everywhere.
  • The types and runtime disagree (a field is undefined you expected, or a 64-bit value is a number not a string). Your protoLoader.loadSync options drifted from the proto-loader-gen-types flags. Keep longs, enums, defaults, and oneofs identical in both places.
  • DeprecationWarning about server.start(). Delete the server.start() call. As of @grpc/grpc-js 1.10.x, bindAsync starts the server; start() is a no-op kept only for backward compatibility.
  • A streaming call hangs and never ends. Your handler never called call.end() (or never stopped writing). Always terminate the stream, and gate writes on call.cancelled so a disconnected client stops the loop.

Next steps

You now have a typed gRPC service with streaming, deadlines, a real error model, and health checks — the shape of a service you can actually run. From here:

  • Add mutual TLS by swapping createInsecure() for grpc.ServerCredentials.createSsl(...) and the matching client credentials. Pair it with the keyless-deploy patterns in the GitHub Actions OIDC guide so no long-lived certs sit in CI.
  • Put the service behind a zero-downtime rollout: the preStop drain and readiness-probe discipline in Kubernetes zero-downtime deployments maps directly onto the grpc.health.v1.Health endpoint you just mounted.
  • Instrument it. The collector setup in the OpenTelemetry Node.js tracing tutorial gives you per-RPC spans, latency, and status-code breakdowns once you add the gRPC instrumentation.

The full source — proto, generated types, server, client, and the six-test suite — runs with npm install, npm run gen, and npm test. Every command in this guide was executed on 14 June 2026 against the pinned versions above.

Sources

Footnotes

  1. @grpc/grpc-js, npm. https://www.npmjs.com/package/@grpc/grpc-js

  2. @grpc/proto-loader, npm. https://www.npmjs.com/package/@grpc/proto-loader

  3. grpc-health-check, npm and README. https://github.com/grpc/grpc-node/blob/master/packages/grpc-health-check/README.md

  4. @grpc/reflection, npm. https://www.npmjs.com/package/@grpc/reflection

  5. TypeScript releases, npm. https://www.npmjs.com/package/typescript

  6. Node.js Release schedule (Node 24 "Krypton" Active LTS through 2028-04-30). https://github.com/nodejs/Release

  7. "Announcing gRPC-JS 1.0" and the grpc package deprecation, gRPC blog / npm. https://grpc.io/blog/grpc-js-1.0/

  8. Protocol Buffers proto3 JSON mapping (64-bit integers encode as strings). https://protobuf.dev/programming-guides/proto3/#json

  9. Error handling and status codes, gRPC docs. https://grpc.io/docs/guides/error/

  10. Metadata, gRPC docs. https://grpc.io/docs/guides/metadata/

  11. gRPC Health Checking Protocol. https://github.com/grpc/grpc/blob/master/doc/health-checking.md

  12. Server.start() deprecation ("No longer needed as of version 1.10.x"), @grpc/grpc-js type definitions. https://github.com/grpc/grpc-node/tree/master/packages/grpc-js