NestJS Enterprise Backend APIs · レッスン

Protobufでのサービスとメッセージの定義

.protoコントラクトを作成し、NestJSマイクロサービス向けの型付きインターフェースを生成します。

レッスン 1/413 ステップ

「Protobufでのサービスとメッセージの定義」はCoddyKit上の無料NestJS Enterprise Backend APIsレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはNestJS Enterprise Backend APIs学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 NestJS Enterprise Backend APIsコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

Why Protobuf Drives the Contract

In a NestJS gRPC microservice, the .proto file is the single source of truth. It defines the wire format, the RPC surface, and — once compiled — the TypeScript types both client and server share.

  • Messages describe the data shapes that travel over the wire.
  • Services declare the callable RPC methods and their request/response messages.

Unlike REST + OpenAPI (where the schema is often written after the code), with gRPC you author the contract first and generate code from it. This is contract-first design.

Anatomy of a .proto File

Every contract starts by declaring the syntax version and a package. The package becomes a namespace that NestJS uses to locate your service at runtime.

  • syntax = "proto3"; — always proto3 for modern gRPC.
  • package billing; — the namespace referenced in the NestJS transport options.

Below is a minimal but complete contract for an invoicing service.

// proto/billing.proto
syntax = "proto3";

package billing;

service InvoiceService {
  rpc GetInvoice (GetInvoiceRequest) returns (Invoice);
}

message GetInvoiceRequest {
  string id = 1;
}

message Invoice {
  string id = 1;
  string customer_id = 2;
  int64 amount_cents = 3;
  string currency = 4;
}

Field Numbers Are the Contract

Each field has a tag number (the = 1, = 2...). These numbers — not the field names — are what gets encoded on the wire.

  • Never reuse or renumber an existing field; doing so breaks binary compatibility with deployed clients.
  • Tags 1–15 use one byte; reserve them for the most frequently sent fields.
  • You may safely rename a field (the name is local to generated code), but never change its number or type.

When you remove a field, mark its number reserved so it is never accidentally recycled.

message Invoice {
  reserved 5, 6;
  reserved "legacy_tax_field";

  string id = 1;
  string customer_id = 2;
  int64 amount_cents = 3;
  string currency = 4;
}

Scalar Types and the int64 Trap

Proto3 scalars map to TypeScript, but the mapping has sharp edges for enterprise APIs handling money or IDs.

  • string → string, bool → boolean, int32/float/double → number.
  • int64, uint64, fixed64 → represented as string (or Long) by most loaders, because JS numbers lose precision beyond 2^53.

For amount_cents as int64, your generated interface should treat it as a string to avoid silent rounding on large values.

// Generated-style interface for the Invoice message
export interface Invoice {
  id: string;
  customerId: string;
  // int64 surfaces as string to preserve precision
  amountCents: string;
  currency: string;
}

snake_case In, camelCase Out

Protobuf convention is snake_case for field names. The NestJS gRPC loader (@grpc/proto-loader) defaults to keepCase: false, which converts fields to camelCase in the generated/runtime objects.

  • customer_id in .proto becomes customerId in TypeScript.
  • If you set keepCase: true, you must read customer_id verbatim — this is a frequent source of undefined bugs.

Pick one convention per project and configure the loader consistently.

// main.ts transport options
import { Transport, GrpcOptions } from '@nestjs/microservices';
import { join } from 'path';

export const grpcOptions: GrpcOptions = {
  transport: Transport.GRPC,
  options: {
    package: 'billing',
    protoPath: join(__dirname, 'proto/billing.proto'),
    loader: { keepCase: false, longs: String, enums: String },
  },
};

Enums and Their Zero-Value Rule

Proto3 enums must define a zero value as the first entry — it is the implicit default when a field is unset on the wire.

  • Name the zero value *_UNSPECIFIED so unset and "first real state" are distinguishable.
  • Enums are open in proto3: a client on a newer schema may send a number your server does not know, so always handle the default branch.

With enums: String in the loader, values arrive as their string names in TypeScript.

enum InvoiceStatus {
  INVOICE_STATUS_UNSPECIFIED = 0;
  INVOICE_STATUS_DRAFT = 1;
  INVOICE_STATUS_SENT = 2;
  INVOICE_STATUS_PAID = 3;
  INVOICE_STATUS_VOID = 4;
}

message Invoice {
  string id = 1;
  InvoiceStatus status = 5;
}

The Service Interface in NestJS

Each rpc method becomes a method on a generated TypeScript interface. Unary RPCs return an Observable (or Promise) of the response message in NestJS.

You typically hand-write or generate an interface and inject the client via ClientGrpc.getService().

import { Observable } from 'rxjs';

export interface GetInvoiceRequest {
  id: string;
}

export interface InvoiceServiceClient {
  getInvoice(request: GetInvoiceRequest): Observable<Invoice>;
}

export interface Invoice {
  id: string;
  customerId: string;
  amountCents: string;
  currency: string;
  status: string;
}

Implementing the Service Handler

On the server side, decorate a controller method with @GrpcMethod. The first argument is the service name from the .proto, the second is the rpc method name.

  • The method receives the decoded request message as a plain object.
  • Return the response message shape directly, or an Observable/Promise of it.
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';

@Controller()
export class InvoiceController {
  @GrpcMethod('InvoiceService', 'GetInvoice')
  getInvoice(data: { id: string }): Invoice {
    return {
      id: data.id,
      customerId: 'cust_42',
      amountCents: '120000',
      currency: 'EUR',
      status: 'INVOICE_STATUS_SENT',
    };
  }
}

Nested Messages and Composition

Messages compose. A field can be another message type, letting you model rich aggregates without flattening everything into one shape.

  • Reference a message type by name; define it before or after — order does not matter within a file.
  • An unset nested message arrives as undefined in TypeScript, so guard before accessing its fields.
message Money {
  int64 amount_cents = 1;
  string currency = 2;
}

message LineItem {
  string sku = 1;
  uint32 quantity = 2;
  Money unit_price = 3;
}

message Invoice {
  string id = 1;
  string customer_id = 2;
  Money total = 3;
  repeated LineItem line_items = 4;
}

Repeated Fields, maps, and Streaming RPCs

Two more building blocks complete most enterprise contracts:

  • repeated T field = N; → a TypeScript array T[]. An empty list and an unset list are indistinguishable on the wire.
  • map<string, T> → a TypeScript record; useful for labels/metadata.

Prefix stream on the request, response, or both to declare server-streaming, client-streaming, or bidirectional RPCs. In NestJS, streaming methods use @GrpcStreamMethod and work with RxJS Observable streams.

service InvoiceService {
  rpc GetInvoice (GetInvoiceRequest) returns (Invoice);
  // server-streaming: many invoices for one query
  rpc ListInvoices (ListInvoicesRequest) returns (stream Invoice);
}

message ListInvoicesRequest {
  string customer_id = 1;
  map<string, string> filters = 2;
}

Generating Typed Interfaces with ts-proto

Hand-writing interfaces drifts from the .proto. Use a generator like ts-proto (via protoc) to emit interfaces, encode/decode helpers, and a NestJS-friendly client/service shape that stays in lockstep with the contract.

  • nestJs=true emits the gRPC service/client interfaces NestJS expects.
  • Run it in CI so a stale generated file fails the build, guaranteeing code matches the contract.
// package.json script
{
  "scripts": {
    "proto:gen": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/generated --ts_proto_opt=nestJs=true,outputServices=grpc-js,useDate=false ./proto/billing.proto"
  }
}

Quick Check: Evolving the Contract

You shipped Invoice with string customer_id = 2;. A new requirement needs you to stop sending customer_id and instead send a richer Customer customer = 6; nested message. Deployed older clients must keep working.

Recap: Contract-First Protobuf

You can now author a service contract and turn it into typed NestJS interfaces:

  • Structure: syntax = proto3, a package namespace, message shapes, and a service with rpc methods.
  • Field numbers are the wire contract — additive evolution only; reserved retired numbers and names.
  • Type mapping: watch int64 → string, snake_case → camelCase, enums need a *_UNSPECIFIED zero value.
  • Composition: nested messages, repeated arrays, map, and stream RPCs.
  • Generation: drive types from the .proto with ts-proto in CI so code can never drift from the contract.
無料で開始

AI チューターと学ぶ TypeScript — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
20
レッスン
76

よくある質問

「Protobufでのサービスとメッセージの定義」レッスンは無料ですか?

はい。「Protobufでのサービスとメッセージの定義」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、NestJS Enterprise Backend APIsコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 NestJS Enterprise Backend APIsコースには全4レッスンが含まれています。

「Protobufでのサービスとメッセージの定義」で何を学びますか?

.protoコントラクトを作成し、NestJSマイクロサービス向けの型付きインターフェースを生成します。 ブラウザで直接実行するハンズオンコードでNestJS Enterprise Backend APIsを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

NestJS Enterprise Backend APIsを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのNestJS Enterprise Backend APIsは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「Protobufでのサービスとメッセージの定義」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このNestJS Enterprise Backend APIsレッスンでコードを書いて実行できますか?

はい。すべてのNestJS Enterprise Backend APIsレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Protobufでのサービスとメッセージの定義
  2. gRPCメソッドの実装と利用
  3. ストリーミングRPCとバックプレッシャー
  4. コントラクトの進化と後方互換性
← NestJS Enterprise Backend APIsに戻る