mrkeyoor.com_
Thu 06 Aug 07:43 UTC
npmUtilsupdated 06 Aug 2026

@bufbuild/protobuf

@bufbuild/protobuf is the runtime half of Protobuf-ES, a Protocol Buffers implementation for JavaScript and TypeScript. You write .proto files, run the protoc-gen-es plugin through the Buf CLI or protoc, and get generated files containing a plain TypeScript type per message plus a schema object describing it. The runtime then provides functions that take a schema and a value: create builds a message with defaults filled in, toBinary and fromBinary handle the wire format, toJson and fromJson handle the canonical JSON mapping, and clone, equals, and merge do the obvious things. Messages are plain objects with plain properties, not class instances with getters and setters, which is the main departure from google-protobuf. It passes the official Protobuf conformance suite, supports proto2, proto3, and Editions, and ships the well-known types, reflection, registries, and extension support in separate entry points so you only pay for what you import.

Verdict

If you are writing new TypeScript against Protobuf schemas, this is the implementation to pick: plain-object messages, conformance-tested serialization, and a small runtime. The one thing it deliberately does not do is read .proto files at runtime, so dynamic-schema use cases still belong to protobufjs.

API stability4/5The v2 API has been stable since its release and 2.x additions have been backward compatible, but the v1 to v2 change was a full rewrite from methods on classes to standalone functions, and v1 still sits on the legacy tag for projects that never made the jump.
Docs5/5protobufes.com covers code generation, plugin options, the message API, JSON, reflection, registries, extensions, and a migration guide, with a runnable example package and published conformance results in the repository.
Maintenance5/5Maintained by Buf as a commercial product's foundation. 2.13.0 published 21 July 2026, repository pushed 6 August 2026, 27 open issues (41 counting PRs).
Ecosystem4/5About 21.9M weekly downloads, largely through connect-es and the Buf toolchain. It is younger than protobufjs and google-protobuf, so third-party integrations and Stack Overflow answers are thinner, though the first-party packages (protoc-gen-es, protoplugin, protovalidate) cover most needs.

Use it if

  • You are consuming a Protobuf or Connect API from TypeScript and want generated types that behave like ordinary objects, so spreading, destructuring, and structural typing all work as expected
  • Wire-format correctness matters to you: it is verified against the official conformance suite, including the JSON mapping and Editions, which several JavaScript implementations only partially handle
  • You are shipping to browsers and care about bytes: the runtime is around 24 KB gzipped with no dependencies, and the entry points for well-known types, reflection, and text format are separate so unused parts drop out
  • You are already using Connect or gRPC-Web through connect-es, which is built on this runtime and expects its generated schema objects
Skip it if

Setup reality

The runtime install is trivial (npm install @bufbuild/protobuf, zero dependencies), and the work is entirely in the generation step. You add @bufbuild/protoc-gen-es and usually @bufbuild/buf as dev dependencies, write a buf.gen.yaml pointing at your proto directory, and run npx buf generate. The default plugin target is js+dts rather than ts, which surprises people who expected .ts files and then find generated JavaScript in their source tree; pass opt: target=ts if that is what you want. import_extension defaults to none, which breaks Node ESM resolution unless you set it to js, and that mismatch produces module-not-found errors that look like a bundler problem rather than a codegen option. Generated files import from @bufbuild/protobuf/codegenv2, so the runtime version and the plugin version have to move together; a stale plugin against a newer runtime fails at import time. Well-known types live at @bufbuild/protobuf/wkt, reflection at /reflect, and text format at /txtpb, so a missing helper usually means you imported from the root entry point instead of a subpath. Sixty-four bit fields come back as bigint, which JSON.stringify refuses to serialize, so use toJson rather than the built-in serializer.

Patterns

Generate TypeScript from .proto filesgenerate-types

# buf.gen.yaml
version: v2
inputs:
  - directory: proto
plugins:
  - local: protoc-gen-es
    out: src/gen
    opt:
      - target=ts
      - import_extension=js

# then
npm install @bufbuild/protobuf
npm install --save-dev @bufbuild/protoc-gen-es @bufbuild/buf
npx buf generate

Without target=ts you get js plus d.ts files, which is the default because it bundles smaller. Set import_extension=js if the generated code will run under Node ESM, otherwise the extensionless imports fail to resolve.

Build a message valuecreate-a-message

import { create } from "@bufbuild/protobuf";
import { UserSchema, type User } from "./gen/user/v1/user_pb.js";

const user: User = create(UserSchema, {
  id: "123",
  firstName: "Alice",
});

user.lastName = "Smith"; // plain property, no setter

create fills in defaults and the internal $typeName field. You can hand-write the object literal instead, but then TypeScript will demand $typeName and you lose default population.

Serialize and parse the wire formatbinary-roundtrip

import { toBinary, fromBinary } from "@bufbuild/protobuf";
import { UserSchema } from "./gen/user/v1/user_pb.js";

const bytes: Uint8Array = toBinary(UserSchema, user);
const parsed = fromBinary(UserSchema, bytes);

Every function takes the schema first and the value second; there are no methods on the message. fromBinary throws on malformed input, so wrap it when parsing anything that came off a network.

Convert to and from canonical JSONjson-roundtrip

import { toJson, toJsonString, fromJson } from "@bufbuild/protobuf";
import { UserSchema } from "./gen/user/v1/user_pb.js";

const obj = toJson(UserSchema, user);            // JsonValue
const text = toJsonString(UserSchema, user);      // string
const back = fromJson(UserSchema, obj);

Do not reach for JSON.stringify on a message: 64-bit fields are bigint and stringify throws on them, and field names would not be mapped to the canonical lowerCamelCase form. toJson handles both.

Work with 64-bit integersint64-fields

import { protoInt64 } from "@bufbuild/protobuf";
import { create } from "@bufbuild/protobuf";
import { InvoiceSchema } from "./gen/billing/v1/invoice_pb.js";

const invoice = create(InvoiceSchema, {
  amountCents: protoInt64.parse("9007199254740993"),
});

const asString = invoice.amountCents.toString();

int64, uint64, sint64, fixed64 and sfixed64 all surface as bigint. protoInt64.parse validates range, which a bare BigInt() call does not. Canonical JSON encodes these as strings, so round-tripping through toJson is safe.

Tell an unset field from a zero valuefield-presence

import { isFieldSet, clearField } from "@bufbuild/protobuf";
import { UserSchema } from "./gen/user/v1/user_pb.js";

if (isFieldSet(user, UserSchema.field.nickname)) {
  console.log(user.nickname);
}

clearField(user, UserSchema.field.nickname);

For plain proto3 scalar fields there is no presence at all and an empty string is indistinguishable from unset. Mark the field optional in the .proto if you need this distinction; then the property becomes T | undefined.

Copy, compare, and combine messagesclone-equals-merge

import { clone, equals, merge } from "@bufbuild/protobuf";
import { UserSchema } from "./gen/user/v1/user_pb.js";

const copy = clone(UserSchema, user);
const same = equals(UserSchema, user, copy); // true
merge(UserSchema, copy, patch); // mutates copy in place

equals compares by Protobuf semantics, not reference, which structuralClone-based comparisons get wrong for bigint and Uint8Array fields. merge follows Protobuf merge rules: repeated fields are appended, not replaced.

Convert between Timestamp and Datetimestamps

import { create } from "@bufbuild/protobuf";
import { timestampNow, timestampFromDate, timestampDate } from "@bufbuild/protobuf/wkt";
import { EventSchema } from "./gen/event/v1/event_pb.js";

const event = create(EventSchema, {
  createdAt: timestampFromDate(new Date("2026-01-01")),
  seenAt: timestampNow(),
});

const js: Date = timestampDate(event.createdAt!);

These helpers live at the /wkt subpath, not the package root. Timestamp keeps seconds as bigint and nanos as number, so converting through Date silently drops sub-millisecond precision.

Put an arbitrary message inside an Anyany-pack-unpack

import { createRegistry } from "@bufbuild/protobuf";
import { anyPack, anyIs, anyUnpack } from "@bufbuild/protobuf/wkt";
import { UserSchema } from "./gen/user/v1/user_pb.js";

const packed = anyPack(UserSchema, user);

if (anyIs(packed, UserSchema)) {
  const back = anyUnpack(packed, UserSchema);
}

// or resolve dynamically
const registry = createRegistry(UserSchema);
const unknown = anyUnpack(packed, registry);

Unpacking without knowing the type needs a registry containing that message, and anyUnpack returns undefined when the type is missing rather than throwing. The same registry is required for toJson on messages containing Any.

Read and write proto2 extensionsextensions

import { hasExtension, getExtension, setExtension } from "@bufbuild/protobuf";
import { priority_ext } from "./gen/ext/v1/ext_pb.js";

if (hasExtension(msg, priority_ext)) {
  console.log(getExtension(msg, priority_ext));
}

setExtension(msg, priority_ext, 5);

Extension values are not stored as normal properties, so they survive round trips only if the extension is registered where you parse. Custom options on descriptors use the parallel getOption and hasOption functions.

Build a registry from a descriptor setregistry-from-descriptors

import { fromBinary, createFileRegistry } from "@bufbuild/protobuf";
import { FileDescriptorSetSchema } from "@bufbuild/protobuf/wkt";
import { readFileSync } from "node:fs";

const set = fromBinary(FileDescriptorSetSchema, readFileSync("image.binpb"));
const registry = createFileRegistry(set);

const schema = registry.getMessage("user.v1.User");

This is the closest thing to runtime schema loading: generate a descriptor set with buf build ahead of time and load that. It still does not parse .proto text, and messages resolved this way are reflection-only, without generated TypeScript types.

Call a service with the generated schemaconnect-client

import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { UserService } from "./gen/user/v1/user_pb.js";

const client = createClient(
  UserService,
  createConnectTransport({ baseUrl: "http://localhost:8080" }),
);

const res = await client.getUser({ id: "123" });

Service descriptors come from the same generated file as the messages, so no extra plugin is needed for Connect. Request arguments are accepted as plain object literals; the client calls create for you.

Alternatives

PackageRegistryPick it when
protobufjsnpmYou need to parse .proto files at runtime or load descriptors dynamically instead of generating code ahead of time
ts-protonpmYou want generated code shaped for a specific stack such as NestJS or @grpc/grpc-js, with heavy per-project codegen options
@connectrpc/connectnpmYou need the RPC layer on top of these messages: typed clients and servers over Connect, gRPC, and gRPC-Web
google-protobufnpmYou are maintaining older code generated by protoc's JavaScript output and are not ready to regenerate