Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the protobuf to typescript engine, best practices for implementation, and data security standards.
Protocol Buffers are the IDL (Interface Description Language) for gRPC — but generating TypeScript from .proto files is where most teams get stuck. There are two generations of tooling: the legacy protoc compiler with various plugins (protoc-gen-ts, ts-protoc-gen), and the modern Buf ecosystem (buf generate with @bufbuild/protoc-gen-es). The Buf approach is significantly simpler to configure, generates idiomatic TypeScript (ES2015+ classes instead of CommonJS), and integrates with Connect-ES for browser-compatible Protobuf over HTTP/1.1. This guide covers the Buf approach, the generated type structure, and how to use generated types to call gRPC services in both Node.js and the browser.
// user.proto (Protobuf v3)
syntax = "proto3";
package users.v1;
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
message User {
string id = 1;
string username = 2;
string email = 3;
Role role = 4;
bool is_active = 5;
repeated string tags = 6;
google.protobuf.Timestamp created_at = 7;
// optional: present = value is set, absent = not provided
optional string bio = 8;
}
enum Role {
ROLE_UNSPECIFIED = 0; // proto3 enum must have a 0 value
ROLE_VIEWER = 1;
ROLE_EDITOR = 2;
ROLE_ADMIN = 3;
}
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser (CreateUserRequest) returns (User);
rpc WatchUsers (WatchUsersRequest) returns (stream User); // server streaming
}
message GetUserRequest { string id = 1; }
message ListUsersRequest { int32 page = 1; int32 limit = 2; Role role_filter = 3; }
message ListUsersResponse { repeated User users = 1; int32 total = 2; }
message CreateUserRequest { string username = 1; string email = 2; Role role = 3; }
message WatchUsersRequest { bool active_only = 1; }
// Generated TypeScript (by @bufbuild/protoc-gen-es)
// user_pb.ts — generated, do not edit
import { Message, proto3 } from "@bufbuild/protobuf";
import { Timestamp } from "@bufbuild/protobuf/dist/cjs/google/protobuf/timestamp_pb";
export enum Role {
UNSPECIFIED = 0,
VIEWER = 1,
EDITOR = 2,
ADMIN = 3,
}
export class User extends Message<User> {
id: string = "";
username: string = "";
email: string = "";
role: Role = Role.UNSPECIFIED;
isActive: boolean = false; // snake_case → camelCase
tags: string[] = [];
createdAt?: Timestamp; // google.protobuf.Timestamp
bio?: string; // optional field — may be undefined
}
# Install Buf
npm install -D @bufbuild/buf @bufbuild/protoc-gen-es @bufbuild/protobuf
# buf.gen.yaml — code generation config
version: v1
plugins:
- plugin: es # generates message types (TypeScript)
out: src/gen
opt: target=ts # TypeScript output
- plugin: connect-es # generates service clients
out: src/gen
opt: target=ts
# buf.yaml — lint and breaking change detection config
version: v1
lint:
use:
- DEFAULT
breaking:
use:
- FILE # check against latest on main branch
# package.json scripts
{
"scripts": {
"proto:generate": "buf generate",
"proto:lint": "buf lint",
"proto:breaking": "buf breaking --against '.git#branch=main'"
}
}
# Generate TypeScript from .proto files
npm run proto:generate
# Outputs:
# src/gen/users/v1/user_pb.ts (message types)
# src/gen/users/v1/user_connect.ts (service client)
// Connect-ES works in both the browser (via fetch) and Node.js (via Node fetch)
import { createPromiseClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { UserService } from "./gen/users/v1/user_connect";
import { Role } from "./gen/users/v1/user_pb";
// Create transport — uses standard fetch, works in browsers
const transport = createConnectTransport({
baseUrl: "https://api.example.com",
});
const client = createPromiseClient(UserService, transport);
// Get a user — request and response are fully typed
async function getUser(id: string) {
const user = await client.getUser({ id });
// user.id: string
// user.username: string
// user.role: Role (enum value)
// user.isActive: boolean
// user.tags: string[]
// user.createdAt: Timestamp | undefined
// user.bio: string | undefined (optional field)
return user;
}
// List users with filter
async function listAdmins() {
const response = await client.listUsers({
page: 1,
limit: 20,
roleFilter: Role.ADMIN,
});
response.users.forEach(u => console.log(u.username));
return response;
}
// Server-side streaming (returns async iterable)
async function watchUsers() {
const stream = client.watchUsers({ activeOnly: true });
for await (const user of stream) {
console.log(`User ${user.username} changed`);
}
}
// For pure gRPC (non-Connect) in Node.js
// buf.gen.yaml — add grpc-node plugin
plugins:
- plugin: es
out: src/gen
opt: target=ts
- plugin: grpc/node
out: src/gen
// Generated client usage
import * as grpc from "@grpc/grpc-js";
import { UserServiceClient } from "./gen/users/v1/user_grpc_pb";
import { GetUserRequest } from "./gen/users/v1/user_pb";
const client = new UserServiceClient(
"localhost:50051",
grpc.credentials.createInsecure()
);
async function getUser(id: string): Promise<User> {
return new Promise((resolve, reject) => {
const req = new GetUserRequest();
req.setId(id);
client.getUser(req, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
}
import { Timestamp } from "@bufbuild/protobuf";
// google.protobuf.Timestamp ↔ JavaScript Date
const tsProto = Timestamp.fromDate(new Date('2024-01-15T08:30:00Z'));
const jsDate = tsProto.toDate();
// google.protobuf.Value — dynamic JSON value in Protobuf
// Useful for metadata fields with unknown structure
import { Value } from "@bufbuild/protobuf";
const value = Value.fromJson({ name: "Alice", active: true });
// google.protobuf.Struct — JSON object
import { Struct } from "@bufbuild/protobuf";
const struct = Struct.fromJson({ key: "value" });
const json = struct.toJson();
// In .proto:
// message Event {
// google.protobuf.Struct metadata = 5; // arbitrary JSON object
// google.protobuf.Value payload = 6; // any JSON value
// }
# buf breaking checks if your .proto changes break existing clients
# Field number changes, field type changes, and service method removals are breaking
# Check against the main branch
buf breaking --against '.git#branch=main'
# Breaking changes that Buf catches:
# - Removing or renaming a field
# - Changing a field number
# - Changing field type (string → int)
# - Removing an enum value used by existing clients
# - Changing a service method signature
# NOT breaking (backwards compatible):
# - Adding a new field (existing clients ignore unknown fields)
# - Adding a new enum value
# - Adding a new RPC method
# - Adding a new message type
# CI/CD: fail the build if breaking changes are detected
buf breaking --against "https://github.com/myorg/myproto.git#branch=main"
reserved 5; in the message to prevent the number from being reused in the future — this prevents silent data corruption when old clients send data with that field number.optional for fields that need presence detection: In proto3, regular fields are always present with default values — you can't distinguish "0" from "not set". Use optional string name = 1; when absence is semantically different from an empty string.Q: What's the difference between protoc-gen-ts and @bufbuild/protoc-gen-es?
A: @bufbuild/protoc-gen-es is the official Buf generator — it produces modern ES module TypeScript with proper class inheritance from Message<T>, tree-shaking support, and browser compatibility. protoc-gen-ts is a community plugin with more varied TypeScript output. For new projects, use the Buf ecosystem.
Q: Can I use Protobuf without gRPC?
A: Yes — Protobuf is just a serialization format. You can use it over REST HTTP/JSON-to-Protobuf gateways, WebSockets, or message queues (Kafka, Pub/Sub). The generated TypeScript classes have toBinary() / fromBinary() and toJson() / fromJson() methods for explicit encoding/decoding.
Q: How do I handle enum default values in proto3?
A: Proto3 requires enums to have a zero value (e.g., ROLE_UNSPECIFIED = 0). When a field is not set, it defaults to 0. Design your enums with a meaningful UNSPECIFIED or UNKNOWN value at 0 so your application code can explicitly handle the "not set" case.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.
Why pasting proprietary company data into third-party web tools is a major liability, and how to stay safe.
A deep dive into combining Zod, React Query, and TypeScript for bulletproof API integration.
Code generation is just the beginning. Discover how a schema-first approach can eliminate 90% of your integration bugs.