0Pricing
NestJS Enterprise Backend APIs · 강의

Protobuf에서 서비스와 메시지 정의

.proto 계약을 작성하고 NestJS 마이크로서비스용 타입이 지정된 인터페이스를 생성합니다.

Protobuf에서 서비스와 메시지 정의은(는) CoddyKit의 무료 NestJS Enterprise Backend APIs 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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.

자주 묻는 질문

“Protobuf에서 서비스와 메시지 정의” 강의는 무료인가요?

네 — “Protobuf에서 서비스와 메시지 정의” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 NestJS Enterprise Backend APIs 강의 전체를 잠금 해제할 수 있습니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

“Protobuf에서 서비스와 메시지 정의”에서 뭘 배우나요?

.proto 계약을 작성하고 NestJS 마이크로서비스용 타입이 지정된 인터페이스를 생성합니다. 브라우저에서 직접 실행하는 실습 코드로 NestJS Enterprise Backend APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

NestJS Enterprise Backend APIs을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 NestJS Enterprise Backend APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“Protobuf에서 서비스와 메시지 정의” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 NestJS Enterprise Backend APIs 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 NestJS Enterprise Backend APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Protobuf에서 서비스와 메시지 정의
  2. gRPC 메서드 구현과 사용
  3. 스트리밍 RPC와 백프레셔
  4. 계약 진화와 하위 호환성
← NestJS Enterprise Backend APIs(으)로 돌아가기