Skip to content
CommunicationDocs 1.3.x

Run the Orders → Inventory Example ​

This is the executable companion to the routing illustration. Orders calls Inventory for stock. Keep the same handler while changing where Inventory runs, then observe what happens when its route is missing.

Run the same example ​

You need Node.js ≥22.13, npm, and a curl build with HTTP/2 support. The complete example pins @connectum/core and the catalog generator at 1.2.0 and sends full protobuf request messages, which every release accepts; it does not require the 1.3 CLI. No broker or telemetry backend is needed.

Create an empty folder and add the six files below. Generate the contracts once, then start both services in one process:

sh
npm install
npm run generate
npm start
Complete example — all six files

Use these paths relative to your project folder. gen/ is created by Buf; do not write the generated catalog by hand.

package.json ​

json
{
    "name": "catalog-walkthrough",
    "private": true,
    "type": "module",
    "scripts": {
        "generate": "buf generate",
        "start": "node --import tsx src/server.ts",
        "typecheck": "tsc --noEmit"
    },
    "engines": {
        "node": ">=22.13.0"
    },
    "dependencies": {
        "@connectum/core": "1.3.0",
        "@bufbuild/protobuf": "2.16.0",
        "@connectrpc/connect": "2.3.0",
        "@connectrpc/connect-node": "2.3.0"
    },
    "devDependencies": {
        "@bufbuild/buf": "1.72.0",
        "@bufbuild/protoc-gen-es": "2.16.0",
        "@connectum/protoc-gen-catalog": "1.3.0",
        "@types/node": "^22.13.0",
        "tsx": "^4.21.0",
        "typescript": "^5.9.3"
    }
}

buf.yaml ​

yaml
version: v2
modules:
  - path: proto
lint:
  use:
    - STANDARD

buf.gen.yaml ​

yaml
version: v2
plugins:
  - local: protoc-gen-es
    out: gen
    opt:
      - target=ts
      - import_extension=.ts
  - local: protoc-gen-connectum-catalog
    out: gen
    strategy: all
    opt:
      - target=ts
      - import_extension=.ts

tsconfig.json ​

json
{
    "compilerOptions": {
        "target": "ES2022",
        "module": "NodeNext",
        "moduleResolution": "NodeNext",
        "strict": true,
        "exactOptionalPropertyTypes": true,
        "verbatimModuleSyntax": true,
        "allowImportingTsExtensions": true,
        "noEmit": true,
        "skipLibCheck": false
    },
    "include": ["src/**/*.ts", "gen/**/*.ts"]
}

proto/shop/v1/catalog.proto ​

proto
syntax = "proto3";

package shop.v1;

service OrdersService {
  rpc CheckAvailability(CheckAvailabilityRequest) returns (CheckAvailabilityResponse);
}

service InventoryService {
  rpc CheckStock(CheckStockRequest) returns (CheckStockResponse);
}

message CheckAvailabilityRequest {
  string sku = 1;
}

message CheckAvailabilityResponse {
  string sku = 1;
  bool available = 2;
}

message CheckStockRequest {
  string sku = 1;
}

message CheckStockResponse {
  int32 units = 1;
}

src/server.ts ​

ts
import { pathToFileURL } from 'node:url';
import { create } from '@bufbuild/protobuf';
import { createGrpcTransport } from '@connectrpc/connect-node';
import { createServer, defineService, mapResolver } from '@connectum/core';
import { serviceCatalog } from '../gen/catalog.gen.ts';
import { CheckStockRequestSchema, InventoryService, OrdersService } from '../gen/shop/v1/catalog_pb.ts';

const inventory = defineService(InventoryService, {
    checkStock: (req) => ({ units: req.sku === 'A-1' ? 7 : 0 }),
});

const orders = defineService(OrdersService, {
    async checkAvailability(req, ctx) {
        const stock = await ctx.call('shop.v1.InventoryService/CheckStock', create(CheckStockRequestSchema, { sku: req.sku }));
        return { sku: req.sku, available: stock.units > 0 };
    },
});

export function createExampleServer(options: { role: 'all' | 'orders' | 'inventory'; port: number; inventoryUrl?: string }) {
    const services = options.role === 'all' ? [orders, inventory] : options.role === 'orders' ? [orders] : [inventory];
    return createServer({
        services,
        catalog: serviceCatalog,
        port: options.port,
        host: '127.0.0.1',
        allowHTTP1: false,
        remoteResolver: mapResolver(options.inventoryUrl ? { [InventoryService.typeName]: createGrpcTransport({ baseUrl: options.inventoryUrl }) } : {}),
        shutdown: { autoShutdown: true, timeout: 5000 },
    });
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
    const role = process.env.ROLE ?? 'all';
    if (role !== 'all' && role !== 'orders' && role !== 'inventory') throw new Error('ROLE must be all, orders or inventory.');
    const inventoryUrl = process.env.INVENTORY_URL?.trim();
    const server = createExampleServer({ role, port: Number(process.env.PORT ?? (role === 'inventory' ? 6000 : 5000)), ...(inventoryUrl ? { inventoryUrl } : {}) });
    await server.start();
    console.log(`${role} listening on ${server.address?.port}`);
}

Check the result ​

In another terminal, call Orders. Its handler calls Inventory for SKU A-1:

sh
curl --http2-prior-knowledge http://127.0.0.1:5000/shop.v1.OrdersService/CheckAvailability \
  -H 'Content-Type: application/json' \
  -H 'Connect-Protocol-Version: 1' \
  -d '{"sku":"A-1"}'

Expected response: {"sku":"A-1","available":true}. Inventory's sample handler returned seven units; Orders turned that into an availability result.

Move Inventory out ​

Stop the combined process. Start Inventory in one terminal and Orders in another:

Terminal 1 · Inventory

sh
ROLE=inventory npm start

Terminal 2 · Orders

sh
ROLE=orders INVENTORY_URL=http://127.0.0.1:6000 npm start

Run the same curl command again. Expect the same response. The Orders handler did not change; its catalog call now crosses the process boundary.

Remove the route ​

Restart only Orders without INVENTORY_URL:

sh
ROLE=orders npm start

The same request now returns an unavailable error (HTTP 503). The contract is still known, but the resolver has no transport for Inventory. Restore the URL and restart Orders to restore the route.

What this example does not provide

This is a localhost teaching example using plaintext HTTP/2. It does not add authentication, business validation, retries or distributed transactions. Local and network calls are not identical security or serialization tests. Configure security, validation and observability when your application needs them.

Keep the full picture ​

The same catalog also supports streaming, inherited deadlines, explicit header propagation and callers outside a handler. These do not need to be learned before the first working call. The complete details are in the configuration and behavior reference.