Skip to main content

Overview

This guide covers creating new Protocol Buffer definitions for adding services to the microservices-app architecture.

Proto File Structure

Follow this structure when creating new proto files:
proto/<service-name>/v1/<service-name>.proto
Example for a greeter service:
proto/greeter/v1/greeter.proto

Creating a Proto File

Step 1: Create Directory Structure

mkdir -p proto/myservice/v1

Step 2: Write Proto Definition

Create proto/myservice/v1/myservice.proto:
syntax = "proto3";

package myservice.v1;

option go_package = "github.com/hackz-megalo-cup/microservices-app/services/gen/go/myservice/v1;myservicev1";

service MyService {
  rpc DoSomething(DoSomethingRequest) returns (DoSomethingResponse) {}
}

message DoSomethingRequest {
  string input = 1;
}

message DoSomethingResponse {
  string output = 1;
}

Step 3: Validate Proto File

Run buf lint to check for style issues:
buf lint

Step 4: Generate Code

Generate Go and TypeScript code:
buf generate
This creates:
  • services/gen/go/myservice/v1/myservice.pb.go
  • services/gen/go/myservice/v1/myserviceconnect/myservice.connect.go
  • frontend/src/gen/myservice/v1/myservice_pb.ts
  • frontend/src/gen/myservice/v1/myservice-MyService_connectquery.ts

Real Service Examples

Example 1: Greeter Service

Simple greeting service with a single RPC method:
syntax = "proto3";

package greeter.v1;

option go_package = "github.com/hackz-megalo-cup/microservices-app/services/gen/go/greeter/v1;greeterv1";

service GreeterService {
  rpc Greet(GreetRequest) returns (GreetResponse) {}
}

message GreetRequest {
  string name = 1;
}

message GreetResponse {
  string message = 1;
  int32 external_status = 2;
  int32 external_body_length = 3;
}
Key points:
  • Package: greeter.v1 (lowercase, versioned)
  • Service: GreeterService (PascalCase with “Service” suffix)
  • RPC method: Greet (PascalCase verb)
  • Messages: GreetRequest / GreetResponse (PascalCase with Request/Response suffix)
  • Fields: name, message (snake_case)
  • Field numbers: Sequential starting from 1

Example 2: Gateway Service

Service for invoking custom endpoints:
syntax = "proto3";

package gateway.v1;

option go_package = "github.com/hackz-megalo-cup/microservices-app/services/gen/go/gateway/v1;gatewayv1";

service GatewayService {
  rpc InvokeCustom(InvokeCustomRequest) returns (InvokeCustomResponse) {}
}

message InvokeCustomRequest {
  string name = 1;
}

message InvokeCustomResponse {
  string message = 1;
}

Example 3: Caller Service

Service for making external HTTP requests:
syntax = "proto3";

package caller.v1;

option go_package = "github.com/hackz-megalo-cup/microservices-app/services/gen/go/caller/v1;callerv1";

service CallerService {
  rpc CallExternal(CallExternalRequest) returns (CallExternalResponse) {}
}

message CallExternalRequest {
  string url = 1;
}

message CallExternalResponse {
  int32 status_code = 1;
  int32 body_length = 2;
}

Naming Conventions

Package Names

  • Format: <service>.v1
  • Case: lowercase
  • Example: greeter.v1, gateway.v1

Service Names

  • Format: <ServiceName>Service
  • Case: PascalCase
  • Suffix: Always end with “Service”
  • Example: GreeterService, GatewayService

RPC Method Names

  • Case: PascalCase
  • Style: Verb or verb phrase
  • Examples: Greet, InvokeCustom, CallExternal

Message Names

  • Format: <MethodName>Request / <MethodName>Response
  • Case: PascalCase
  • Example: GreetRequest, GreetResponse

Field Names

  • Case: snake_case
  • Examples: name, status_code, body_length

Go Package Option

The go_package option must follow this format:
option go_package = "<import-path>/<service>/v1;<package-name>";
For the microservices-app project:
option go_package = "github.com/hackz-megalo-cup/microservices-app/services/gen/go/<service>/v1;<service>v1";
Examples:
  • greeter.v1github.com/.../greeter/v1;greeterv1
  • gateway.v1github.com/.../gateway/v1;gatewayv1
  • caller.v1github.com/.../caller/v1;callerv1

Field Numbering

Field numbers are permanent identifiers for serialization:
message Example {
  string field_one = 1;   // First field
  int32 field_two = 2;    // Second field
  bool field_three = 3;   // Third field
}

Rules

  • Start at 1 (not 0)
  • Must be unique within a message
  • Numbers 1-15 use 1 byte encoding (use for frequent fields)
  • Numbers 16-2047 use 2 bytes
  • Never reuse field numbers (breaks backward compatibility)
  • Reserve deleted field numbers:
message Example {
  reserved 2, 4, 5;  // Deleted fields
  reserved "old_field_name";  // Deleted field names
  string current_field = 1;
}

Advanced Patterns

Nested Messages

Define messages within other messages:
message User {
  string id = 1;
  Address address = 2;
  
  message Address {
    string street = 1;
    string city = 2;
    string zip_code = 3;
  }
}

Repeated Fields

Represent lists or arrays:
message ListUsersResponse {
  repeated User users = 1;
}

Enums

Define enumerated types:
enum Status {
  STATUS_UNSPECIFIED = 0;  // Default value, always include
  STATUS_PENDING = 1;
  STATUS_ACTIVE = 2;
  STATUS_INACTIVE = 3;
}

message User {
  string id = 1;
  Status status = 2;
}

Optional Fields

In proto3, use optional for nullable fields:
message User {
  string id = 1;
  optional string nickname = 2;  // Can be null
}

Timestamps

Use well-known types from google.protobuf:
import "google/protobuf/timestamp.proto";

message Event {
  string id = 1;
  google.protobuf.Timestamp created_at = 2;
}

Integrating with Go Services

After generating code, implement the service in Go:
// services/internal/myservice/service.go
package myservice

import (
    "context"
    "connectrpc.com/connect"
    myservicev1 "github.com/hackz-megalo-cup/microservices-app/services/gen/go/myservice/v1"
    "github.com/hackz-megalo-cup/microservices-app/services/gen/go/myservice/v1/myserviceconnect"
)

type Service struct{}

func (s *Service) DoSomething(
    ctx context.Context,
    req *connect.Request[myservicev1.DoSomethingRequest],
) (*connect.Response[myservicev1.DoSomethingResponse], error) {
    res := &myservicev1.DoSomethingResponse{
        Output: "Processed: " + req.Msg.Input,
    }
    return connect.NewResponse(res), nil
}
Register the service:
// services/cmd/myservice/main.go
func main() {
    mux := http.NewServeMux()
    service := &myservice.Service{}
    path, handler := myserviceconnect.NewMyServiceHandler(service)
    mux.Handle(path, handler)
    http.ListenAndServe(":8080", mux)
}

Using in TypeScript Frontend

Generated React Query hooks provide type-safe API access:
import { useDoSomething } from '@/gen/myservice/v1/myservice-MyService_connectquery';

function MyComponent() {
  const { data, isLoading, error } = useDoSomething({
    input: 'test'
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return <div>{data?.output}</div>;
}

Testing Proto Changes

1. Lint Check

buf lint

2. Breaking Change Detection

buf breaking --against main

3. Code Generation

buf generate

4. Build Services

cd services && go build ./...

5. Build Frontend

cd frontend && npm run build

Common Issues

Lint Failures

Problem: buf lint reports style violations Solution: Follow naming conventions:
  • Package: lowercase with version (myservice.v1)
  • Service: PascalCase with “Service” suffix
  • RPC: PascalCase verb
  • Messages: PascalCase with Request/Response suffix
  • Fields: snake_case

Import Errors in Go

Problem: Go can’t find generated packages Solution: Check go_package option matches:
option go_package = "github.com/hackz-megalo-cup/microservices-app/services/gen/go/<service>/v1;<service>v1";

TypeScript Type Errors

Problem: Frontend shows type errors after proto changes Solution: Regenerate code and rebuild:
buf generate
cd frontend && npm run build

Next Steps

Build docs developers (and LLMs) love