Open-source project
protobufjs/protobuf.js avatar
protobufjs/protobuf.js

protobuf.js: Protocol Buffers in JavaScript Without protoc

High-performance Protocol Buffers for JavaScript and TypeScript. Conformant through Edition 2026, and unusually versatile. No protoc required.

10,595 stars1,845 forksJavaScriptNOASSERTION

At a glance

What is it?
protobuf.js is a JavaScript and TypeScript implementation of Protocol Buffers that loads .proto schemas at runtime or generates static code, with no protoc step. Here is how it works, where it fits, and what to check before adopting it.
Who is it for?
Adopt protobuf.js when your services already speak Protocol Buffers and you need a JavaScript or TypeScript side that loads .proto files at runtime or generates static code without protoc. Skip it if you only exchange JSON with browsers, if you cannot bound the size of untrusted payloads, or if you need a stable package version rather than the moving 8.x line.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What protobuf.js Solves for JavaScript Teams

Protocol Buffers is a binary serialization format defined by .proto schema files. The reference toolchain is protoc, a compiler that reads those schemas and emits code in a target language. For JavaScript and TypeScript projects that means an extra build step, a binary to install on every developer machine and CI runner, and generated code that has to be kept in sync with the schema. protobuf.js removes protoc from that loop. The package reads .proto files directly at runtime, or its companion CLI turns them into static JavaScript and matching TypeScript declarations ahead of time. The README describes it as a JavaScript implementation of Protocol Buffers for Node.js and browsers, independently maintained with contributions from the upstream Protocol Buffers project. The audience is specific: teams whose backend, mobile clients or gRPC services already speak Protobuf, and who need a JavaScript or TypeScript participant that can encode and decode the same wire format. It is less interesting if both ends of your communication are JavaScript and you control both, because then you are paying schema ceremony for a format you chose yourself.

Runtime Reflection Versus Generated Code

There are two ways to use the library, and the choice shapes your build. The runtime path loads a schema and looks types up by name. The README gives this shape: call protobuf.load on a .proto file, then root.lookupType with the fully qualified message name. Imports inside the schema resolve relative to the importing file by default; to resolve against a specific base directory you create a Root and override root.resolvePath before calling root.load. The generated path moves that work to build time. The CLI package produces reflection bundles, static code and TypeScript declarations, and the package.json build:types script shows the same tool, cli/bin/pbts, being used on the library's own sources. Static code avoids parsing .proto text on every process start and gives you declarations the compiler can check. Runtime loading keeps the schema as the single source of truth and lets you swap it without rebuilding. A service that starts once and runs for days will not notice the parse cost; a serverless function that cold-starts on every request might. The README does not publish a comparison of the two paths, so measure your own startup before assuming either is free.

Installing protobuf.js and Encoding a First Message

The package installs from npm under the name protobufjs. The CLI is a separate add-on package, protobufjs-cli, installed as a development dependency. The README's example schema declares proto3 syntax, a package named awesomepackage, and one message with a single string field. Note the naming rule: protobuf.js converts .proto field names to camelCase by default, so awesome_field becomes awesomeField in JavaScript. The keepCase option, passed when loading or parsing, preserves the names as written.

bash
npm install protobufjs
npm install --save-dev protobufjs-cli

The schema file for the walkthrough, exactly as the README shows it:

proto
syntax = "proto3";

package awesomepackage;

message AwesomeMessage {
  string awesome_field = 1;
}

Loading the schema and looking up the type is asynchronous in the README's example. loadSync exists for synchronous loading on Node.js, and load also accepts a callback.

ts
const protobuf = require("protobufjs");

const root = await protobuf.load("awesome.proto");
const AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage");

Encoding and decoding are symmetric. encode returns a Writer, and you call finish() on it to get the buffer. decode accepts a Reader or a Uint8Array. The README is explicit that encode does not verify input implicitly, and points to verify for plain objects whose shape is not guaranteed, create for message instances built from already valid data, and fromObject when converting broader JavaScript objects.

ts
const payload = { awesomeField: "hello" };

const message = AwesomeMessage.create(payload);

const encoded = AwesomeMessage.encode(message).finish();
const decoded = AwesomeMessage.decode(encoded);

After running this you should have a Uint8Array in encoded and a message instance in decoded whose awesomeField is "hello". If the schema lives somewhere other than the working directory, that is the point to set up a Root and override resolvePath rather than passing an absolute path to load.

The Conversion Boundary and Unknown Fields

The most opinionated part of the design is where it draws the line between protobuf types and ordinary JavaScript. fromObject and toObject are described in the README as an explicit interoperability boundary, not a convenience wrapper. fromObject accepts enum values by name, base64 bytes, decimal 64-bit strings, Long and BigInt. toObject takes ConversionOptions that decide what comes back: longs as BigInt, String or Number, enums as names, bytes as base64 strings, plus defaults, arrays, objects and oneofs to include unset or empty values. That last group matters more than it looks. Without defaults: true, an unset field is absent from the returned object rather than present with its default, which changes how downstream code branches. The longs: Number option is documented as possibly losing precision, which is the honest framing: 64-bit integers do not fit in a JavaScript number. Two further details are easy to miss. Map keys are the string representation of the value, or an 8-character hash string for 64-bit keys, so a map with integer keys does not round-trip to the same key type you put in. And unknown fields present on the wire are discarded by default, described as the safer choice for memory. You can retain them with reader.discardUnknown = false per reader, or Reader.discardUnknown = false to change the default for readers created afterward, and drop them later with delete message.$unknowns. If you forward messages between services and rely on fields a newer schema added, that default will silently strip them unless you opt in.

Memory Behaviour on Untrusted Input

The README makes a point that many serialization libraries leave implicit: decoded structures occupy more memory than their encoded form, because a binary buffer becomes a tree of JavaScript objects. It states directly that applications processing untrusted input should apply appropriate input-size and concurrency limits to bound memory use. That is a design constraint, not a bug, and it is the reason unknown fields are dropped by default. The practical consequence is that a length-delimited stream from an untrusted peer needs a cap on frame size before you hand bytes to decode, and a server accepting many concurrent decodes needs a limit on how many run at once. The library does not enforce either for you. This is also the point where protobuf.js is the wrong tool: if your input arrives from an untrusted client at unbounded size and you have no place to put a limit, a schema-based binary decoder adds a memory amplifier you did not have with a streaming JSON parser that discards as it reads. The repository does not ship a decoder option that enforces a maximum message size, so the limit has to live in your transport layer.

protobuf.js CLI, Browser Builds and the protoc Question

The CLI is the answer to the most common objection about JavaScript Protobuf tooling: that it drags protoc into your build. The README calls protobufjs-cli a JS-native toolchain that does not require setting up protoc, and notes that if you prefer a protoc-based workflow it provides protoc-gen-pbjs as an option. So both routes exist, and the choice is about where code generation happens rather than whether it can. On the browser side, canonical builds are served from the jsDelivr CDN under the dist/ path, supporting CommonJS, AMD and a global window.protobuf. The README's advice is to pin an exact version in production, which is worth taking literally: an unpinned CDN URL resolves to whatever the latest 8.x is at request time. The package.json browser field maps fs to false, which is how the same entry point works in both environments. For an alternative approach, protobuf-ts is the obvious comparison. It generates TypeScript-first code from .proto files through protoc plugins, so the output is idiomatic TypeScript with no runtime reflection layer, but it requires protoc or a compatible plugin host in the build. protobuf.js instead keeps a reflection runtime in the package and offers generation as an option. If you want zero runtime dependency and full static typing, protobuf-ts matches that goal more directly. If you want to load a schema at runtime, or avoid protoc entirely, protobuf.js is the one that does it.

Maintenance, Releases and Licence

The repository is not archived, and the last push was on 2026-09-19, two days before this article's date, so calling it actively maintained is accurate. The release cadence is visible in the tags: protobufjs v8.8.0 and v7.6.6 both landed on 2026-08-27, alongside protobufjs-cli v2.7.0. Two major lines receiving releases on the same day is the fact that matters for upgrade planning. The 7.x line is still being patched while 8.x moves forward, so a team on 7.x has a supported path but should expect to migrate eventually; the README does not document a migration guide for 7 to 8, so treat that as work you will do from the changelog. The repository uses release-please, visible in .release-please-manifest.json and release-please-config.json, which means release notes are generated from conventional commits. The package.json declares the licence as BSD-3-Clause and engines as node >= 12.0.0, while the repository metadata reports NOASSERTION. The package manifest is the more specific statement, but the discrepancy is worth resolving with your own legal review before you rely on either. The README also asks commercial dependents to consider sponsorship, and frames it as funding bug fixes, releases, LTS and security handling. That is a maintenance-model signal rather than a licence term, but it tells you where support capacity comes from. The upgrade cost itself is low: the runtime is a normal npm dependency and the CLI is a dev dependency, so a version bump is a lockfile change plus whatever the changelog flags.

Editorial conclusion

Adopt protobuf.js when your services already speak Protocol Buffers and you need a JavaScript or TypeScript side that loads .proto files at runtime or generates static code without protoc. Skip it if you only exchange JSON with browsers, if you cannot bound the size of untrusted payloads, or if you need a stable package version rather than the moving 8.x line. Before committing, install protobufjs and protobufjs-cli, run pbjs against one real schema, and check the generated TypeScript declarations against your tsconfig.

Frequently asked questions

What is protobuf.js?

It is a JavaScript implementation of Protocol Buffers for Node.js and browsers, maintained independently with contributions from the upstream Protocol Buffers project. It loads .proto schemas without requiring protoc and supports both runtime reflection and specialized code generation with matching TypeScript declarations.

Is Protobuf better than JSON?

The README does not compare protobuf.js with JSON, so it makes no claim about which is better. What it does state is that Protobuf is a structured binary format, and that decoded structures incur runtime memory overhead beyond their encoded representation, which is a cost JSON tooling does not share in the same way.

What are the downsides of using Protobuf?

The README names several. Decoded structures use more memory than their encoded form, so untrusted input needs size and concurrency limits. Unknown fields are discarded by default, which can strip fields a newer schema added unless you set reader.discardUnknown = false. And 64-bit values do not fit in a JavaScript number, so longs: Number may lose precision.

What is Protobuf and what is it used for?

Protocol Buffers is a binary serialization format defined by .proto schema files, and protobuf.js is a JavaScript implementation of it for Node.js and browsers. It is used to encode and decode messages against a shared schema, with runtime reflection or generated static code and matching TypeScript declarations.

Is Protobuf still used?

protobuf.js is not archived and the last push to the repository was on 2026-09-19. Releases continue across two lines: protobufjs v8.8.0 and v7.6.6 both shipped on 2026-08-27, alongside protobufjs-cli v2.7.0.

Official sources

  1. Issues
  2. protobufjs/protobuf.js on GitHub
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/protobufjs-protobuf-js.svg)](https://hysenlabs.com/projects/protobufjs-protobuf-js)