google-protobuf vs grpc-web vs protobufjs vs ts-proto
Building Type-Safe RPC and Serialization in TypeScript Frontends
google-protobufgrpc-webprotobufjsts-protoSimilar Packages:

Building Type-Safe RPC and Serialization in TypeScript Frontends

These packages address protocol buffer serialization and remote procedure calls (RPC) in JavaScript and TypeScript. google-protobuf and protobufjs handle binary serialization of data structures. grpc-web provides a client for making gRPC-Web calls over HTTP/1.1. ts-proto is a code generator that creates TypeScript classes from .proto files. Together, they form the core toolkit for type-safe communication between frontends and protobuf-backed backends.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
google-protobuf0470927 kB705 months ago(BSD-3-Clause AND Apache-2.0)
grpc-web09,24464.8 kB1693 days agoApache-2.0
protobufjs010,5773.7 MB8625 days agoBSD-3-Clause
ts-proto02,587797 kB172a month agoISC

google-protobuf vs grpc-web vs protobufjs vs ts-proto: Architecture and DX Compared

When building modern frontends that communicate with protobuf-backed services, you need to choose how to serialize data and how to transport it. google-protobuf and protobufjs handle the serialization, grpc-web handles the transport protocol, and ts-proto improves the TypeScript experience. Let's break down how they differ in real-world usage.

📦 Creating and Using Messages

The way you instantiate and access data differs significantly between the runtimes and generators.

google-protobuf uses classes generated by the official protoc plugin. Accessors are method-based.

// google-protobuf: Method-based access
const user = new proto.User();
user.setName("Alice");
const name = user.getName();

protobufjs can work dynamically or with static classes. Static usage looks like plain objects.

// protobufjs: Property-based access
const user = User.create({ name: "Alice" });
const name = user.name;

ts-proto generates idiomatic TypeScript classes with plain properties.

// ts-proto: Idiomatic TS classes
const user = User.fromPartial({ name: "Alice" });
const name = user.name;

grpc-web wraps these messages in a client call. It typically expects google-protobuf messages.

// grpc-web: Wrapping message in call
const request = new UserRequest();
request.setUser(user);
client.getUser(request, metadata, callback);

🚀 Transport and RPC Calls

Choosing how to send data over the network is where grpc-web stands apart, while the others focus on serialization.

google-protobuf does not handle transport. You serialize to binary and send via fetch or XHR.

// google-protobuf: Manual transport
const bytes = user.serializeBinary();
fetch('/api/user', { method: 'POST', body: bytes });

protobufjs also requires manual transport setup or integration with a RPC client.

// protobufjs: Manual transport
const buffer = User.encode(user).finish();
fetch('/api/user', { method: 'POST', body: buffer });

ts-proto generates helpers but relies on your fetch implementation for transport.

// ts-proto: Transport via fetch
const bytes = User.encode(user).finish();
await fetch('/api/user', { method: 'POST', body: bytes });

grpc-web provides the RPC client implementation specifically for gRPC-Web protocol.

// grpc-web: Built-in RPC client
const call = client.getUser(request, { customHeader: 'value' });
call.on('data', response => console.log(response));

🛡️ TypeScript Integration and Types

Developer experience varies wildly when working with TypeScript definitions.

google-protobuf provides external type definitions. They often feel disconnected from the implementation.

// google-protobuf: External types
import { User } from './proto/user_pb';
// Types may lag behind implementation or require specific compiler flags

protobufjs has community-maintained types. Static code generation improves this.

// protobufjs: Static types
import { User } from './generated';
// Good support, but sometimes requires manual type assertions

ts-proto is built for TypeScript first. Types are generated alongside implementation.

// ts-proto: Native TS types
import { User } from './generated';
// Types match implementation exactly, including optional fields

grpc-web types depend on the message library used (usually google-protobuf).

// grpc-web: Dependent types
import { UserClient } from './proto/user_grpc_web_pb';
// Types tie you to the google-protobuf message structure

🏗️ Build Process and Setup

Getting these tools running in your build pipeline involves different levels of complexity.

google-protobuf requires running protoc with the JS plugin during build.

# google-protobuf: protoc command
protoc --js_out=import_style=commonjs:. *.proto

protobufjs can load .proto files at runtime or use pbjs to generate static code.

# protobufjs: pbjs command
pbjs -t static-module -w commonjs -o bundle.js *.proto

ts-proto runs as a plugin during protoc execution or via npm scripts.

# ts-proto: protoc with plugin
protoc --ts_proto_out=. *.proto

grpc-web requires protoc-gen-grpc-web to generate the client stubs.

# grpc-web: grpc-web plugin
protoc --grpc-web_out=import_style=typescript:. *.proto

⚠️ Maintenance and Ecosystem Status

Understanding the long-term viability of each package is critical for architectural decisions.

google-protobuf is the official library but is often considered legacy for new web projects. It is in maintenance mode.

// google-protobuf: Legacy status
// Still supported, but community has shifted to lighter alternatives

protobufjs is community-driven and actively maintained. It is the standard for JS/TS.

// protobufjs: Active maintenance
// Frequent updates and strong community support

ts-proto is actively maintained and popular in the TypeScript community.

// ts-proto: Active development
// Regularly updated to support latest TS features

grpc-web is maintained by the gRPC team. It is stable for gRPC-Web usage.

// grpc-web: Stable
// Required if using gRPC-Web protocol specifically

🌱 When Not to Use These

These tools are powerful, but they add complexity. Consider alternatives when:

  • You need simple JSON APIs: Standard fetch with JSON is often enough for public APIs.
  • You lack backend protobuf support: Don't introduce protobuf just for the frontend.
  • You need browser streaming without gRPC: Standard WebSockets or Server-Sent Events might be simpler.

📌 Summary Table

PackageRoleTypeScript DXTransportMaintenance
google-protobufRuntime⚠️ External defsNone🟡 Legacy
protobufjsRuntime✅ GoodNone🟢 Active
ts-protoGenerator🌟 ExcellentNone🟢 Active
grpc-webClient⚠️ Tied to runtimegRPC-Web🟢 Stable

💡 Final Recommendation

Think in terms of serialization and transport separately.

  • For Serialization: Use ts-proto with protobufjs runtime for new TypeScript projects. It offers the best balance of type safety and bundle size. Avoid google-protobuf unless you are tied to legacy systems.
  • For Transport: Use grpc-web only if your backend explicitly serves gRPC-Web endpoints. Otherwise, standard fetch with protobuf serialization (via ts-proto or protobufjs) is often simpler to deploy and debug.

Final Thought: These tools solve specific problems in the protobuf ecosystem. ts-proto and protobufjs form the modern baseline for serialization, while grpc-web is a specialized transport client. Choose based on your backend's capabilities and your team's need for type safety.

How to Choose: google-protobuf vs grpc-web vs protobufjs vs ts-proto

  • google-protobuf:

    Choose google-protobuf if you are maintaining a legacy project that already depends on it or if you strictly require the official Google JavaScript runtime for compatibility with existing Closure Compiler workflows. It is less ideal for new TypeScript projects due to heavier bundle sizes and less idiomatic type support.

  • grpc-web:

    Choose grpc-web if your backend serves gRPC-Web endpoints and you need to make unary or streaming calls directly from the browser without a BFF (Backend for Frontend). It is required when using the standard gRPC-Web protocol over HTTP/1.1.

  • protobufjs:

    Choose protobufjs if you want a pure JavaScript/TypeScript runtime that is actively maintained, lighter weight, and offers both dynamic and static code options. It is the community standard for web projects not strictly tied to the official Google JS runtime.

  • ts-proto:

    Choose ts-proto if you prioritize developer experience and want idiomatic TypeScript classes generated from your .proto files. It is best for new projects that want strong types without the boilerplate of official Google generated code.

README for google-protobuf

Protocol Buffers - Google's data interchange format

Copyright 2008 Google Inc.

This directory contains the JavaScript Protocol Buffers runtime library.

The library is currently compatible with:

  1. CommonJS-style imports (eg. var protos = require('my-protos');)
  2. Closure-style imports (eg. goog.require('my.package.MyProto');)

Support for ES6-style imports is not implemented yet. Browsers can be supported by using Browserify, webpack, Closure Compiler, etc. to resolve imports at compile time.

To use Protocol Buffers with JavaScript, you need two main components:

  1. The protobuf runtime library. You can install this with npm install google-protobuf, or use the files in this directory. If npm is not being used, as of 3.3.0, the files needed are located in binary subdirectory; arith.js, constants.js, decoder.js, encoder.js, map.js, message.js, reader.js, utils.js, writer.js
  2. The Protocol Compiler protoc. This translates .proto files into .js files. The compiler is not currently available via npm, but you can download a pre-built binary on GitHub (look for the protoc-*.zip files under Downloads).

Project Status

As of v4.0.0, you can directly install the protoc-gen-js plugin from npm as @protocolbuffers/protoc-gen-js.

Support Status

We currently do not have staffing for more than minimal support for this open source project. We will answer questions and triage any issues.

Contributing

Contributions should preserve existing behavior where possible. Current customers rely on applications continuing to work across minor version upgrades. We encourage small targeted contributions. Thanks!

Setup

First, obtain the Protocol Compiler. The easiest way is to download a pre-built binary from https://github.com/protocolbuffers/protobuf/releases.

If you want, you can compile protoc from source instead. To do this follow the instructions in the top-level README.

Once you have protoc compiled, you can run the tests provided along with our project to examine whether it can run successfully. In order to do this, you should download the Protocol Buffer source code from the release page with the link above. Then extract the source code and navigate to the folder named js containing a package.json file and a series of test files. In this folder, you can run the commands below to run the tests automatically.

$ npm install
$ PROTOC_INC=/usr/include/google/protobuf npm test

PROTOC_INC specifies the protobuf include path. By default, we use protoc located from PATH. Optionally, you can use the PROTOC enviroment variable to specify an alternative protoc.

This will run two separate copies of the tests: one that uses Closure Compiler style imports and one that uses CommonJS imports. You can see all the CommonJS files in commonjs_out/. If all of these tests pass, you know you have a working setup.

Using Protocol Buffers in your own project

To use Protocol Buffers in your own project, you need to integrate the Protocol Compiler into your build system. The details are a little different depending on whether you are using Closure imports or CommonJS imports:

Closure Imports

If you want to use Closure imports, your build should run a command like this:

$ protoc --js_out=library=myproto_libs,binary:. messages.proto base.proto

For Closure imports, protoc will generate a single output file (myproto_libs.js in this example). The generated file will goog.provide() all of the types defined in your .proto files. For example, for the unit tests the generated files contain many goog.provide statements like:

goog.provide('proto.google.protobuf.DescriptorProto');
goog.provide('proto.google.protobuf.DescriptorProto.ExtensionRange');
goog.provide('proto.google.protobuf.DescriptorProto.ReservedRange');
goog.provide('proto.google.protobuf.EnumDescriptorProto');
goog.provide('proto.google.protobuf.EnumOptions');

The generated code will also goog.require() many types in the core library, and they will require many types in the Google Closure library. So make sure that your goog.provide() / goog.require() setup can find all of your generated code, the core library .js files in this directory, and the Google Closure library itself.

Once you've done this, you should be able to import your types with statements like:

goog.require('proto.my.package.MyMessage');

var message = proto.my.package.MyMessage();

If unfamiliar with Closure or its compiler, consider reviewing Closure documentation.

CommonJS imports

If you want to use CommonJS imports, your build should run a command like this:

$ protoc --js_out=import_style=commonjs,binary:. messages.proto base.proto

For CommonJS imports, protoc will spit out one file per input file (so messages_pb.js and base_pb.js in this example). The generated code will depend on the core runtime, which should be in a file called google-protobuf.js. If you are installing from npm, this file should already be built and available. If you are running from GitHub, you need to build it first by running:

$ gulp dist

Once you've done this, you should be able to import your types with statements like:

var messages = require('./messages_pb');

var message = new messages.MyMessage();

The --js_out flag

The syntax of the --js_out flag is:

--js_out=[OPTIONS:]output_dir

Where OPTIONS are separated by commas. Options are either opt=val or just opt (for options that don't take a value). The available options are specified and documented in the GeneratorOptions struct in generator/js_generator.h.

Some examples:

  • --js_out=library=myprotos_lib.js,binary:.: this contains the options library=myprotos.lib.js and binary and outputs to the current directory. The import_style option is left to the default, which is closure.
  • --js_out=import_style=commonjs,binary:protos: this contains the options import_style=commonjs and binary and outputs to the directory protos. import_style=commonjs_strict doesn't expose the output on the global scope.

API

The API is not well-documented yet. Here is a quick example to give you an idea of how the library generally works:

var message = new MyMessage();

message.setName("John Doe");
message.setAge(25);
message.setPhoneNumbers(["800-555-1212", "800-555-0000"]);

// Serializes to a UInt8Array.
var bytes = message.serializeBinary();

var message2 = MyMessage.deserializeBinary(bytes);

For more examples, see the tests. You can also look at the generated code to see what methods are defined for your generated messages.