---
title: 'gRPC in Node.js and TypeScript: Production Guide (2026)'
description: 'Build a typed, production gRPC service in Node.js and TypeScript: server streaming, deadlines, the error model, and health checks. Runnable 2026 guide.'
url: https://nerdleveltech.com/grpc-nodejs-typescript-production-guide
language: en
date: '2026-06-14'
category: backend
tags:
  - grpc
  - node.js
  - typescript
  - microservices
  - protobuf
  - backend
  - rpc
  - api design
markdown_url: https://nerdleveltech.com/grpc-nodejs-typescript-production-guide.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/grpc-nodejs-typescript-production-guide'
terms: https://nerdleveltech.com/llms.txt
---

# 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.4**[^grpcjs], **`@grpc/proto-loader` 0.8.1**[^protoloader], **`grpc-health-check` 2.1.0**[^health], and **`@grpc/reflection` 1.0.4**[^reflection] on **TypeScript 6.0.3**[^ts] and **Node.js 24**. 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)[^node] 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` instead[^deprecation]. `@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:

```bash
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@24.13.1
```

`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`:

```json
{
  "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:

```bash
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`:

```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 strings[^proto3json]. 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`:

```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:

```bash
npm run gen
```

The flags matter. `--longs=String` is what turns those `int64` fields into TypeScript `string`s (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:

```typescript
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:

```typescript
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):

```typescript
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:

```typescript
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 rest[^errors]. 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 trailers[^metadata], 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 speak[^healthproto]:

```typescript
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 warning[^start]:

```typescript
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:

```bash
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:

```typescript
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:

```typescript
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:

```bash
npm run client
```

```text
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`:

```typescript
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:

```bash
npm run typecheck   # tsc --noEmit, exits 0
npm test
```

```text
# 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:

```bash
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](https://nerdleveltech.com/github-actions-oidc-aws-terraform-keyless-deploys-tutorial) 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](https://nerdleveltech.com/kubernetes-zero-downtime-deployments-tutorial) maps directly onto the `grpc.health.v1.Health` endpoint you just mounted.
- Instrument it. The collector setup in [the OpenTelemetry Node.js tracing tutorial](https://nerdleveltech.com/opentelemetry-collector-nodejs-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

[^grpcjs]: `@grpc/grpc-js`, npm. https://www.npmjs.com/package/@grpc/grpc-js
[^protoloader]: `@grpc/proto-loader`, npm. https://www.npmjs.com/package/@grpc/proto-loader
[^health]: `grpc-health-check`, npm and README. https://github.com/grpc/grpc-node/blob/master/packages/grpc-health-check/README.md
[^reflection]: `@grpc/reflection`, npm. https://www.npmjs.com/package/@grpc/reflection
[^ts]: TypeScript releases, npm. https://www.npmjs.com/package/typescript
[^node]: Node.js Release schedule (Node 24 "Krypton" Active LTS through 2028-04-30). https://github.com/nodejs/Release
[^deprecation]: "Announcing gRPC-JS 1.0" and the `grpc` package deprecation, gRPC blog / npm. https://grpc.io/blog/grpc-js-1.0/
[^proto3json]: Protocol Buffers proto3 JSON mapping (64-bit integers encode as strings). https://protobuf.dev/programming-guides/proto3/#json
[^errors]: Error handling and status codes, gRPC docs. https://grpc.io/docs/guides/error/
[^metadata]: Metadata, gRPC docs. https://grpc.io/docs/guides/metadata/
[^healthproto]: gRPC Health Checking Protocol. https://github.com/grpc/grpc/blob/master/doc/health-checking.md
[^start]: `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
