Publish protobuf as an NPM package
In this blog, we will generate TypeScript files from the protobuf files and publish them as an npm package. We will be using the protobuf files that were created in my last post.
The protobuf files are in a monorepo, causing some issues when containerizing the services with docker. Thus we will publish these protobuf files to a registry as an npm package and use the registry package inside the services instead of the symlink.
Some sections of this blog will be about versioning and publishing packages from a monorepo. You can skip those parts if you don't have a monorepo.
To manage the versions of the packages we will use changesets. There are other tools like lerna etc which can do same thing, but I prefer changesets. Read more about changesets here
We will install the @changesets/cli package and run the init command. This will create the config.json file for changesets inside the .changeset/ directory.
npm install @changesets/cli && npx changeset init
Lets change the current working directory to the protos package. We will make few changes in package.json and build.sh files.
cd packages/protos
First, we will update the output path for TypeScript files to dist/ts. We are doing this so that we can support other languages as well.
build.sh
#!/bin/bash
protoc --plugin=$(npm root)/.bin/protoc-gen-ts_proto \
--ts_proto_out=dist/ts \
--ts_proto_opt=outputServices=grpc-js \
--ts_proto_opt=esModuleInterop=true \
-I=src/ src/**/*.proto
We also need to install 2 packages as dev dependencies. mkdirp and rimraf. We need them to clean the dist/ts directory before every build.
npm i -D mkdirp rimraf
Then in the package.json file, we will add the prebuild and postbuild scripts. The pre and post prefix scripts auto-run before and after the script with the matching name. Read more about them here.
package.json
"scripts": {
"prebuild": "rimraf dist/ts && mkdirp dist/ts",
"build": "./build.sh",
"postbuild": "cp package.json ./dist/ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
The prebuild script will delete the dist/ts and recreate it. So every time we run the build script, we will get new files and not any files from previous builds.
The postbuild script will copy the package.json file to the /dist/ts folder. As we will publish only that folder as an npm package. This is because the current directory will have other files as well like .proto files, and other language generate files. We don't want them in the npm package. Plus the import paths will look clean as the files are next to the package.json file.
We will publish the protos package to npm with github actions. Thus we need to create a workflow file to build and release the package. The file should be in the .github/workflows directory otherwise the action will not run.
mkdir -p .github/workflows
touch .github/workflows/proto-npm-release.yml
In the workflow file, we will configure that the action will only run when a tag with the prefix as the package name is pushed. We will create a job named publish in the file. In the Setup Node step, we will pass registry-url and scope. This will create a .npmrc file that uses the NODE_AUTH_TOKEN environment variable for auth. So update the org-name as per your npm org. And in the Publish packages, we will change the current directory to packages/protos/dist/ts and run the npm publish command from there.
.github/workflows/proto-npm-release.yml
name: Release
on:
push:
tags:
- "@<org-name>/protos@*"
jobs:
publish:
name: Build and Publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "16.x"
registry-url: "https://registry.npmjs.org/"
scope: "@<org-name>"
- name: Install Protoc
uses: arduino/setup-protoc@master
with:
version: "3.x"
- name: Install and Build Packages
run: |
cd packages/protos
npm install
npm run build
- name: Publish packages
run: |
cd packages/protos/dist/ts
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
We need to generate a token in npm to publish the packages.

After generating the token, we need to create an environment variable in the github repo. The name of the variable will be NODE_AUTH_TOKEN. and copy the npm token as the secret here.

After this, we just need to create and publish a new tag to github and then github action will publish the package to the npm registry.
We will use using changesets to create a new tag. It will update the version in the package.json file and create a tag with the package name & version. If you don't want to use changesets then you can manually bump the package.json version and create a tag.
First, we need to run the npx changeset command from the root of the repo root. It will ask which package we need to update and what is the bump type.
npx changesetThen we need to run the version command on the changesets. This will auto update the package.json versions.
npx changeset versionThen we need to run the tag command on the changesets. This will create the tags for the version updates. If n number of packages are updated then n number of tags will be created with their respective version. After that, we need to push the tags to the git repo.
npx changeset tag
git push --follow-tagsThis will trigger the github action workflow and the package will be published to the npm. Then in the dependent apps, we need to uninstall the old local package and install the new one.
cd services/product-service
npm uninstall @nodejs-microservices/protos
We also need to remove the packages/protos from the root package.json workspaces, delete the root node_modules directory, and the lockfiles and reinstall the package. This will install the package from the registry instead of the local symlink one.
npm i @rsbh-nodejs-microservices/protosI have changed the package name from @nodejs-microservices/protos to @rsbh-nodejs-microservices/protos as there is already an org with the name nodejs-microservices. We also need to update the package name and the import paths in the files as well.
src/main.ts
- import { ProductServiceService } from "@nodejs-microservices/protos/dist/product/product";
+ import { ProductServiceService } from "@rsbh-nodejs-microservices/protos/product/product";
src/server.ts
- import {
- CreateProductRequest,
- CreateProductResponse,
- GetProductRequest,
- GetProductResponse,
- ListProductsRequest,
- ListProductsResponse,
- Product,
- ProductServiceServer,
- } from "@nodejs-microservices/protos/dist/product/product";
+ import {
+ CreateProductRequest,
+ CreateProductResponse,
+ GetProductRequest,
+ GetProductResponse,
+ ListProductsRequest,
+ ListProductsResponse,
+ Product,
+ ProductServiceServer,
+ } from "@rsbh-nodejs-microservices/protos/product/product";
After removing the protos package from the root package.json workspaces, the changesets command will not show it during the version update. We need to manually copy it again in the package.json file and remove it after updating the version.
All the source code for this blog is available on GitHub.