How to Use Protocol Buffers in TypeScript

A practical guide to working with Protocol Buffers in TypeScript and Node.js

Published: January 2025 • 8 min read

Protocol Buffers works great with TypeScript, giving you type safety and efficient serialization. Whether you're building Node.js backends, web applications, or React Native apps, protobuf provides a solid foundation for data exchange.

This guide shows you how to use Protocol Buffers in TypeScript projects using ts-proto, the most popular TypeScript protobuf generator.

What You'll Need

  • Node.js: Version 14 or higher
  • TypeScript: Version 4.x or higher
  • Protocol Buffer Compiler: We'll install this next

Step 1: Install Dependencies

Create a new TypeScript project:

mkdir protobuf-ts-example
cd protobuf-ts-example
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init

Install protobuf dependencies:

npm install ts-proto protobufjs
npm install protoc-gen-ts-proto --save-dev

Install protoc compiler:

# On macOS
brew install protobuf

# On Windows
choco install protoc

# On Linux
sudo apt install protobuf-compiler

Step 2: Set Up Project Structure

Create the following structure:

protobuf-ts-example/
├── package.json
├── tsconfig.json
├── proto/
│   └── person.proto
├── src/
│   ├── generated/
│   └── index.ts

Step 3: Create Your .proto File

Create proto/person.proto:

syntax = "proto3";

package person;

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
  repeated string phone_numbers = 4;
  
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
  
  repeated PhoneNumber phones = 5;
  bool is_active = 6;
}

message AddressBook {
  repeated Person people = 1;
}

Step 4: Generate TypeScript Code

Add a generation script to package.json:

{
  "scripts": {
    "generate": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/generated --ts_proto_opt=esModuleInterop=true proto/*.proto",
    "start": "ts-node src/index.ts"
  }
}

Run the generator:

npm run generate

Step 5: Use Protocol Buffers in TypeScript

Create src/index.ts:

import { Person, AddressBook } from './generated/person';
import * as fs from 'fs';

// Create a Person
const person: Person = {
  name: 'John Doe',
  id: 1234,
  email: 'john.doe@example.com',
  phoneNumbers: ['555-1234', '555-5678'],
  phones: [
    {
      number: '555-9999',
      type: Person_PhoneType.MOBILE,
    },
  ],
  isActive: true,
};

console.log('Created Person:');
console.log(`Name: ${person.name}`);
console.log(`ID: ${person.id}`);
console.log(`Email: ${person.email}`);

// Serialize to bytes
const bytes = Person.encode(person).finish();
console.log(`\nSerialized to ${bytes.length} bytes`);

// Deserialize from bytes
const decoded = Person.decode(bytes);
console.log('\nDeserialized Person:');
console.log(`Name: ${decoded.name}`);
console.log(`Active: ${decoded.isActive}`);

// Save to file
fs.writeFileSync('person.bin', bytes);
console.log('\nSaved to person.bin');

// Read from file
const fileData = fs.readFileSync('person.bin');
const fromFile = Person.decode(fileData);
console.log(`Read from file: ${fromFile.name}`);

// Create AddressBook
const addressBook: AddressBook = {
  people: [
    person,
    {
      name: 'Jane Smith',
      id: 5678,
      email: 'jane@example.com',
      phoneNumbers: [],
      phones: [],
      isActive: true,
    },
  ],
};

console.log(`\nAddress book has ${addressBook.people.length} people`);

// Convert to JSON
const json = Person.toJSON(person);
console.log('\nJSON representation:');
console.log(JSON.stringify(json, null, 2));

// Parse from JSON
const fromJson = Person.fromJSON({
  name: 'Bob',
  id: 9999,
  email: 'bob@example.com',
  isActive: true,
});
console.log(`\nCreated from JSON: ${fromJson.name}`);

Step 6: Run Your Application

npm start

Expected output:

Created Person:
Name: John Doe
ID: 1234
Email: john.doe@example.com

Serialized to 78 bytes

Deserialized Person:
Name: John Doe
Active: true

Saved to person.bin
Read from file: John Doe

Address book has 2 people

Common Operations

Type-Safe Access

// TypeScript knows all fields and types
const name: string = person.name;
const id: number = person.id;
const phones: Person_PhoneNumber[] = person.phones;

Partial Updates

const updated: Person = {
  ...person,
  email: 'newemail@example.com',
};

Validate with Zod (Optional)

// Generate with --ts_proto_opt=useExactTypes=false
// to get runtime validation schemas

Stream Encoding

import { Writer } from 'protobufjs/minimal';

const writer = new Writer();
Person.encode(person, writer);
const bytes = writer.finish();

Best Practices for TypeScript

Use Strict Type Checking

Enable strict: true in tsconfig.json to catch type errors at compile time.

Don't Edit Generated Files

Generated TypeScript files are recreated each time. Add src/generated/ to your .gitignore.

Use ts-proto Options

Customize generation with options like esModuleInterop,useOptionals, andoneof=unions.

Using with Express

import express from 'express';
import { Person } from './generated/person';

const app = express();
app.use(express.raw({ type: 'application/x-protobuf' }));

app.post('/person', (req, res) => {
  const person = Person.decode(req.body);
  console.log('Received:', person.name);
  
  const response = Person.encode(person).finish();
  res.type('application/x-protobuf').send(response);
});

app.listen(3000);

Common Issues

Issue: protoc-gen-ts_proto not found

Solution: Use the full path in your script:./node_modules/.bin/protoc-gen-ts_proto

Issue: Module not found errors

Solution: Make sure to install both ts-proto andprotobufjs.

Issue: Type errors in generated code

Solution: Update ts-proto to the latest version:npm update ts-proto

Related Tools

Conclusion

Protocol Buffers with TypeScript gives you the best of both worlds: efficient binary serialization and strong type safety. The ts-proto library generates clean, idiomatic TypeScript code that feels natural to use.

This combination works great for Node.js services, web applications, and even browser-based apps where you need efficient data exchange with type safety.