@bufbuild/protobuf review
@bufbuild/protobuf 2.14.0 is the runtime used by code generated from Protocol Buffer schemas for JavaScript and TypeScript. It creates plain message objects, reads and writes the binary wire format, follows the Protobuf JSON mapping, and exposes reflection, registries, extensions, text format, and well-known types. It does not read .proto source at runtime. Release 2.14.0 rebuilt several hot paths: the binary writer now grows one buffer, message creators are compiled and cached, and encoding uses new ASCII and base64 fast paths. The release also removes Node 20 support. In our install, both ESM import and CommonJS require worked and the package supplied its own declarations.
@bufbuild/protobuf 2.14.0 installed in 1.5 seconds as one 3 MB package with 0 audit findings, working ESM and CommonJS entry points, and bundled types. It is a good fit for generated TypeScript schemas and Connect code, but protobufjs is the clearer choice when .proto files must be parsed at runtime, and Node 20 deployments cannot take this release.
We installed it
| Install | ✓ · 1.5s | 1 package on disk · 3 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 27.8 KB | gzipped (103.4 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does @bufbuild/protobuf install cleanly?
Yes. In a fresh container with an empty cache, npm install @bufbuild/protobuf finished in 2 seconds, leaving 1 package and 3 MB on disk. npm audit reported no known vulnerabilities.
How much does @bufbuild/protobuf add to a browser bundle?
27.8 KB gzipped (103.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does @bufbuild/protobuf work with both ESM and CommonJS?
Yes. Both import '@bufbuild/protobuf' and require('@bufbuild/protobuf') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does @bufbuild/protobuf include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
@bufbuild/protobuf or protobufjs: which should you use?
protobufjs: Use it when the application must parse .proto text or JSON descriptors at runtime. @bufbuild/protobuf 2.14.0 installed in 1.5 seconds as one 3 MB package with 0 audit findings, working ESM and CommonJS entry points, and bundled types.
When should you not use @bufbuild/protobuf?
The process must parse user-supplied .proto text while it runs. This runtime accepts generated schemas or descriptor sets; protobufjs has a runtime parser.
Use it if
- You generate TypeScript from .proto files and want fields exposed as ordinary object properties instead of getter and setter methods.
- The application needs Protobuf binary and JSON conversion that is tested against the official conformance suite.
- Reflection, descriptor registries, custom options, extensions, or well-known types need to share one runtime with generated messages.
- You are building Connect clients or servers and want the schema objects used by the Connect packages.
- The process must parse user-supplied .proto text while it runs. This runtime accepts generated schemas or descriptor sets; protobufjs has a runtime parser.
- Your team will not add a schema-generation step. Normal use requires protoc-gen-es through Buf or protoc, and generated files must be refreshed after schema changes.
- Production still runs Node 20. Version 2.14.0 dropped it, while the README promises only maintained Node releases.
- Existing code depends on google-protobuf message classes, instance serialization methods, or getFoo and setFoo calls. Protobuf-ES v2 uses schema-first functions and plain objects.
- The browser support window extends beyond Baseline releases from the last 2.5 years, or the project pins a TypeScript version more than two years old. Those cases fall outside the stated compatibility policy.
Setup reality
Our no-cache install of @bufbuild/protobuf 2.14.0 finished in 1.5 seconds inside an unprivileged Node 22 Bookworm container. It left one package and 3 MB on disk. The package was 2,816 KB unpacked, declared 0 direct dependencies and 0 peer dependencies, and npm audit found 0 known vulnerabilities. It is an ESM package with an exports map, yet both require() and ESM import passed. TypeScript declarations ship in the package.
Installing the runtime does not turn .proto files into modules. Add @bufbuild/protoc-gen-es and either Buf or protoc, put the generator in buf.gen.yaml, and regenerate after every schema edit. The generated modules import runtime code, so keep generator and runtime versions compatible. TypeScript output uses target=ts; Node ESM projects commonly set import_extension=js so emitted imports resolve after compilation. None of this needs credentials unless the schemas come from a private registry.
Our esbuild check imported the entire package root and produced 103.4 KB minified, or 27.8 KB gzipped. A real bundle can be smaller when tree shaking removes unused exports, but that depends on the exact imports. Well-known types, reflection, wire helpers, and text format have separate export paths. The install ran without native compilation or a postinstall hook.
Protobuf rules cause the first runtime surprises. A 64-bit integer is represented as bigint, so JSON.stringify cannot serialize a message containing one; use toJson() or toJsonString(). An ordinary proto3 scalar does not record presence unless the schema marks it optional. Dynamic operation requires a compiled descriptor set and createFileRegistry(), which still does not parse .proto text. Version 2.14.0 no longer supports Node 20, so check runtime images before upgrading.
Patterns
Generate TypeScript from local schemas generate-typescript
# buf.gen.yaml
version: v2
inputs:
- directory: proto
plugins:
- local: protoc-gen-es
out: src/gen
opt:
- target=ts
- import_extension=js
# install and generate
npm install @bufbuild/protobuf
npm install --save-dev @bufbuild/protoc-gen-es @bufbuild/buf
npx buf generateVersion 2 generation emits schema objects used by the runtime functions. import_extension=js is needed by many Node ESM builds.
Create a message from an initializer create-message
import { create } from '@bufbuild/protobuf';
import { UserSchema } from './gen/user_pb.js';
const user = create(UserSchema, {
id: 'user-42',
displayName: 'Mira',
});
user.displayName = 'Mira Singh';create() takes the generated schema first and returns a plain object with the message metadata expected by the runtime.
Write a message to the wire format encode-binary
import { toBinary } from '@bufbuild/protobuf';
import { UserSchema } from './gen/user_pb.js';
const bytes = toBinary(UserSchema, user);
await fileHandle.write(bytes);Version 2 messages do not have a serializeBinary() or toBinary() instance method. Pass the schema and value to the free function.
Decode untrusted binary input decode-binary
import { fromBinary } from '@bufbuild/protobuf';
import { UserSchema } from './gen/user_pb.js';
try {
const user = fromBinary(UserSchema, requestBody);
console.log(user.id);
} catch (error) {
return new Response('invalid protobuf', { status: 400 });
}fromBinary() throws on malformed wire data. Catch that failure at the network or file boundary.
Use the Protobuf JSON mapping read-write-json
import { fromJsonString, toJsonString } from '@bufbuild/protobuf';
import { UserSchema } from './gen/user_pb.js';
const json = toJsonString(UserSchema, user);
const decoded = fromJsonString(UserSchema, json);These helpers apply Protobuf rules for field names, enums, bytes, and 64-bit integers. JSON.stringify() cannot handle bigint values.
Set a 64-bit integer without losing precision set-int64
import { create, protoInt64 } from '@bufbuild/protobuf';
import { InvoiceSchema } from './gen/invoice_pb.js';
const invoice = create(InvoiceSchema, {
sequence: protoInt64.parse('9007199254740993'),
});64-bit integer fields use bigint. protoInt64.parse() checks the string value against the field's supported range.
Distinguish an optional field from its default check-presence
import { clearField, isFieldSet } from '@bufbuild/protobuf';
import { UserSchema } from './gen/user_pb.js';
const field = UserSchema.field.nickname;
if (isFieldSet(user, field)) {
console.log(user.nickname);
}
clearField(user, field);Presence exists only when the schema supports it. Mark a proto3 scalar optional if unset must differ from its zero value.
Copy and compare messages clone-compare
import { clone, equals } from '@bufbuild/protobuf';
import { UserSchema } from './gen/user_pb.js';
const copy = clone(UserSchema, user);
copy.displayName = 'Changed';
console.log(equals(UserSchema, user, copy));equals() compares message values, including byte arrays by content. clone() prevents edits to the copy from mutating the original.
Merge a message patch into a copy merge-patch
import { clone, merge } from '@bufbuild/protobuf';
import { UserSchema } from './gen/user_pb.js';
const next = clone(UserSchema, current);
merge(UserSchema, next, incomingPatch);merge() mutates the target and follows Protobuf merge rules. Repeated fields append instead of replacing the existing array.
Convert between Timestamp and Date convert-timestamp
import { timestampDate, timestampFromDate } from '@bufbuild/protobuf/wkt';
const sentAt = timestampFromDate(new Date('2026-08-26T10:00:00Z'));
const nativeDate = timestampDate(sentAt);Timestamp helpers are exported from /wkt. JavaScript Date stores milliseconds, so nanosecond precision is lost on conversion.
Pack and verify an Any value pack-any
import { anyIs, anyPack, anyUnpack } from '@bufbuild/protobuf/wkt';
import { UserSchema } from './gen/user_pb.js';
const packed = anyPack(UserSchema, user);
if (anyIs(packed, UserSchema)) {
const unpacked = anyUnpack(packed, UserSchema);
console.log(unpacked?.id);
}Unpacking by dynamic type name requires a registry containing that schema. anyUnpack() returns undefined when the type does not match.
Build a reflection registry from descriptors load-descriptor-set
import { createFileRegistry, fromBinary } from '@bufbuild/protobuf';
import { FileDescriptorSetSchema } from '@bufbuild/protobuf/wkt';
import { readFile } from 'node:fs/promises';
const bytes = await readFile('schema.binpb');
const descriptorSet = fromBinary(FileDescriptorSetSchema, bytes);
const registry = createFileRegistry(descriptorSet);
const schema = registry.getMessage('acme.user.v1.User');Compile schema.binpb before deployment. A descriptor registry supports reflection but does not create static TypeScript types or parse .proto source.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| protobufjs | npm | Use it when the application must parse .proto text or JSON descriptors at runtime. |
| google-protobuf | npm | Keep it when a mature codebase already uses protoc's JavaScript message classes and getter APIs. |
| @protobuf-ts/runtime | npm | Choose it when protobuf-ts already generates the project and its reflection model is part of the design. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

