Google API Annotations
Add HTTP/REST annotations for web clients
Status: ✅ Full Support
Example: examples/js-example/
JavaScript/TypeScript support provides multiple output formats and RPC options for modern web development.
| Plugin | Description | Generated Files |
|---|---|---|
protoc-gen-js | CommonJS messages | *_pb.js, *_pb.d.ts |
protoc-gen-es | ES modules | *.js, *.d.ts |
protoc-gen-grpc-web | gRPC-Web client | *_grpc_web_pb.js |
protoc-gen-twirp_js | Twirp RPC | *_twirp.js |
languages.js = { enable = true; outputPath = "src/proto";};languages.js = { enable = true; outputPath = "src/proto"; packageName = "@myorg/proto"; options = [ "import_style=commonjs" "binary" ];
# Modern ECMAScript modules es = { enable = true; options = [ "target=ts" # Generate TypeScript "import_extension=.js" # ES module extensions "json_types=true" # JSON type definitions ]; };
# gRPC-Web for browser compatibility grpcWeb = { enable = true; options = [ "import_style=typescript" "mode=grpcwebtext" "format=text" ]; };
# Twirp RPC framework twirp = { enable = true; options = ["lang=typescript"]; };};Control exactly which proto files JavaScript/TypeScript processes using files and additionalFiles:
Use Case: Frontend needs Google API annotations for REST mapping, but backend doesn’t.
# Global files (shared by all languages)protoc.files = [ "./proto/common/v1/types.proto" "./proto/common/v1/status.proto"];
languages = { # JavaScript: Extend global files with frontend-specific files js = { enable = true; additionalFiles = [ "./proto/api/v1/user_api.proto" # Public REST APIs "./proto/api/v1/auth_api.proto" # Authentication "./proto/google/api/annotations.proto" # HTTP annotations "./proto/google/api/http.proto" # REST definitions ]; es.enable = true; };
# Go: Use only internal services (excludes Google annotations) go = { enable = true; files = [ "./proto/common/v1/types.proto" "./proto/internal/v1/user_service.proto" # No Google annotations - prevents linting errors ]; grpc.enable = true; };};Use Case: JavaScript only needs public API definitions for web clients.
languages.js = { enable = true; outputPath = "src/proto/client"; # Override: Only public APIs + shared types files = [ "./proto/common/v1/types.proto" "./proto/api/v1/public_api.proto" "./proto/api/v1/auth_api.proto" "./proto/google/api/annotations.proto" # No internal services - client security boundary ]; es.enable = true;};Use Case: Add browser-specific protobuf definitions to the standard set.
languages.js = { enable = true; # Use global files + these additional browser-specific files additionalFiles = [ "./proto/browser/v1/analytics.proto" "./proto/browser/v1/notifications.proto" "./proto/browser/v1/offline_storage.proto" "./proto/third_party/google/protobuf/timestamp.proto" ]; es.enable = true; grpcWeb.enable = true;};| Option | Type | Description | Example |
|---|---|---|---|
files | list | Override global files completely | Web client APIs only |
additionalFiles | list | Extend global files | Add Google annotations |
Google API Annotations
Add HTTP/REST annotations for web clients
Frontend Boundaries
Prevent frontend access to internal backend services
Browser-Specific APIs
Include browser-only protobuf definitions
Third-Party Protos
Add external protobuf dependencies
syntax = "proto3";
package example.v1;
message User { string id = 1; string name = 2; string email = 3; int32 age = 4; repeated string tags = 5; map<string, string> metadata = 6;
oneof status { ActiveStatus active = 7; InactiveStatus inactive = 8; }}
message ActiveStatus { string since = 1; string activity = 2;}
message InactiveStatus { string reason = 1; string until = 2;}
service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); rpc StreamUsers(StreamUsersRequest) returns (stream User);}
message GetUserRequest { string id = 1;}
message GetUserResponse { User user = 1;}
message ListUsersRequest { int32 page_size = 1; string page_token = 2; repeated string tags = 3;}
message ListUsersResponse { repeated User users = 1; string next_page_token = 2; int32 total_count = 3;}
message CreateUserRequest { User user = 1;}
message CreateUserResponse { User user = 1;}
message StreamUsersRequest { repeated string tags = 1;}import { UserServiceClient } from "./proto/example/v1/example_grpc_web_pb";import { GetUserRequest, CreateUserRequest, User } from "./proto/example/v1/example_pb";
// Create gRPC-Web clientconst client = new UserServiceClient("https://api.example.com");
// Enable CORS credentialsconst metadata = { "authorization": "Bearer token" };
// Create userconst createRequest = new CreateUserRequest();const user = new User();user.setId("1");user.setName("John Doe");user.setEmail("john@example.com");user.setAge(30);user.setTagsList(["developer", "typescript"]);
const metadataMap = user.getMetadataMap();metadataMap.set("department", "Engineering");
createRequest.setUser(user);
client.createUser(createRequest, metadata, (err, response) => { if (err) { console.error("Error:", err.message); return; } console.log("Created user:", response.getUser()?.getName());});
// Get user with promisesconst getRequest = new GetUserRequest();getRequest.setId("1");
const promise = new Promise((resolve, reject) => { client.getUser(getRequest, metadata, (err, response) => { if (err) { reject(err); } else { resolve(response); } });});
promise .then((response: any) => { console.log("User:", response.getUser()?.toObject()); }) .catch((err) => { console.error("Failed:", err); });
// Stream usersconst streamRequest = new StreamUsersRequest();streamRequest.setTagsList(["active"]);
const stream = client.streamUsers(streamRequest, metadata);
stream.on("data", (user) => { console.log("Streamed:", user.getName());});
stream.on("error", (err) => { console.error("Stream error:", err);});
stream.on("end", () => { console.log("Stream ended");});import { User, ActiveStatus } from "./proto/example/v1/example_pb.js";import { create, toBinary, fromBinary, toJson, fromJson } from "@bufbuild/protobuf";
// Create messages with ES modulesconst user = create(UserSchema, { id: "1", name: "John Doe", email: "john@example.com", age: 30, tags: ["developer"], metadata: { role: "senior" }, status: { case: "active", value: { since: "2024-01-01", activity: "coding" } }});
// Binary serializationconst bytes = toBinary(UserSchema, user);console.log(`Binary size: ${bytes.length} bytes`);
// Binary deserializationconst decoded = fromBinary(UserSchema, bytes);console.log("Decoded:", decoded);
// JSON serializationconst json = toJson(UserSchema, user);console.log("JSON:", JSON.stringify(json, null, 2));
// JSON deserializationconst fromJsonUser = fromJson(UserSchema, json);console.log("From JSON:", fromJsonUser);
// Type-safe field accessif (user.status.case === "active") { console.log("Active since:", user.status.value.since);}
// Working with repeated fieldsuser.tags.push("javascript", "protobuf");
// Working with mapsuser.metadata["team"] = "Platform";
// Iterate map entriesfor (const [key, value] of Object.entries(user.metadata)) { console.log(`${key}: ${value}`);}import { UserService } from "./proto/example/v1/example_twirp";import { User } from "./proto/example/v1/example_pb";
// Create Twirp clientconst client = new UserService.Client("https://api.example.com");
// Set custom headersclient.setHeaders({ "Authorization": "Bearer token"});
async function useTwirp() { try { // Create user const user: User = { id: "1", name: "John Doe", email: "john@example.com", age: 30, tags: ["developer"], metadata: { location: "Remote" }, active: { since: "2024-01-01", activity: "coding" } };
const createResponse = await client.CreateUser({ user: user });
console.log("Created:", createResponse.user);
// Get user const getResponse = await client.GetUser({ id: "1" }); console.log("Retrieved:", getResponse.user);
// List users const listResponse = await client.ListUsers({ pageSize: 10, pageToken: "", tags: ["developer"] });
console.log("Users:", listResponse.users);
} catch (error) { if (error.code) { // Twirp error console.error(`Twirp error ${error.code}: ${error.msg}`); } else { // Network error console.error("Network error:", error); } }}
useTwirp();{ "compilerOptions": { "target": "ES2022", "module": "ES2022", "moduleResolution": "node", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "paths": { "@/proto/*": ["./src/proto/*"] } }, "include": ["src/**/*"], "exclude": ["src/proto/**/*_pb.js"]}{ "scripts": { "proto": "bufrnix", "build": "tsc", "dev": "tsx watch src/index.ts", "test": "vitest" }, "dependencies": { "@bufbuild/protobuf": "^1.0.0" }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.0.0", "tsx": "^4.0.0", "vitest": "^1.0.0" }}export default { optimizeDeps: { include: [ "@bufbuild/protobuf", ], },};import { useEffect, useState } from "react";import { User } from "@/proto/example/v1/example_pb";
// Use your preferred RPC framework (gRPC-Web, Twirp, etc.)// This example shows the React component structureexport function UserList() { const [users, setUsers] = useState<User[]>([]); const [loading, setLoading] = useState(true);
useEffect(() => { // Fetch users using your RPC client fetchUsers() .then((response) => { setUsers(response.users); setLoading(false); }) .catch((error) => { console.error("Failed to load users:", error); setLoading(false); }); }, []);
if (loading) return <div>Loading...</div>;
return ( <ul> {users.map((user) => ( <li key={user.id}> {user.name} - {user.email} </li> ))} </ul> );}target=ts for better type safety.js extensions for proper ES module supportadditionalFiles to include Google API annotations for REST clientscd examples/js-examplenix developnpm installnpm run buildnpm startFor ES modules, ensure proper extensions:
// Correctimport { User } from "./proto/example_pb.js";
// Incorrectimport { User } from "./proto/example_pb";For browser clients, configure appropriate CORS settings on your server and include authentication headers as needed by your RPC framework (gRPC-Web, Twirp, etc.).
Configure path mapping in tsconfig.json:
{ "compilerOptions": { "paths": { "@/proto/*": ["./src/proto/*"] } }}{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
bufrnix = {
url = "github:conneroisu/bufrnix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
nixpkgs,
flake-utils,
bufrnix,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
packages = [
pkgs.nodejs
pkgs.nodePackages.typescript
];
};
packages = {
default = bufrnix.lib.mkBufrnixPackage {
inherit pkgs;
config = {
root = ./.;
protoc = {
sourceDirectories = ["./proto"];
includeDirectories = ["./proto"];
files = ["./proto/example/v1/example.proto"];
};
languages.js = {
enable = true;
outputPath = "proto/gen/js";
packageName = "example-proto";
# Per-language file control (new feature)
# files = [
# "./proto/common/v1/types.proto"
# "./proto/api/v1/user_api.proto"
# ];
# additionalFiles = [
# "./proto/google/api/annotations.proto" # For Connect-ES REST clients
# "./proto/google/api/http.proto"
# ];
# Modern JavaScript with ECMAScript modules
es = {
enable = true;
};
};
};
};
};
});
}