Releasing a closed-source, reusable JavaScript/TypeScript package for internal use across frontend and backend is a common challenge, especially when you want to automate it, keep things modular, and avoid unnecessary leakage of code. Here’s how I do it, step by step, using only what’s needed for a stable, repeatable workflow. Why GitHub Packages (and Not npmjs.org)? Why GitHub Packages (and Not npmjs.org)? Most teams reach for npmjs.org by default, but if your utilities are strictly internal - or have some private contract processing logic you’re not ready to open-source - GitHub’s own registry is more than enough: Integrated with your repository: No extra accounts or keys to manage. Scoped access: Control exactly who gets your code. Familiar workflows: Your team’s already on GitHub; why hop away? Integrated with your repository: No extra accounts or keys to manage. Integrated with your repository: Scoped access: Control exactly who gets your code. Scoped access: Familiar workflows: Your team’s already on GitHub; why hop away? Familiar workflows: I've used this for smart contract SDKs referenced by both the frontend app and the NestJS API. Directory Structure Directory Structure I keep only my distributable code in /package, separate from internal scripts/docs, to avoid accidentally leaking dev files. /package |-- .github/ |-- src/ |-- package/ # <--- Only your published files live here |-- package.json |-- dist/ |-- index.js |-- ... |-- .github/ |-- src/ |-- package/ # <--- Only your published files live here |-- package.json |-- dist/ |-- index.js |-- ... Pro tip: npm publish runs only in /package, not at the repo root. Pro tip: npm publish /package Manual Releases Triggered From GitHub Releases Manual Releases Triggered From GitHub Releases Every package update is explicitly tagged as a release in GitHub's UI, which helps prevent accidental releases of incomplete work. Action Workflow File Action Workflow File Below is the full workflow that gets the job done. name: Publish package on GitHub Packages on: release: types: [created] jobs: publish: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 registry-url: "https://npm.pkg.github.com" scope: "@your-user-name" always-auth: true - name: Install dependencies run: npm ci - name: Build package run: npm run package - name: Install package dependencies working-directory: ./package run: npm ci - name: Publish package working-directory: ./package run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} name: Publish package on GitHub Packages on: release: types: [created] jobs: publish: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 registry-url: "https://npm.pkg.github.com" scope: "@your-user-name" always-auth: true - name: Install dependencies run: npm ci - name: Build package run: npm run package - name: Install package dependencies working-directory: ./package run: npm ci - name: Publish package working-directory: ./package run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} Place it in github/workflows/publish.yml github/workflows/publish.yml Key Parts Key Parts Scopes, Not Monorepos: No workspaces, no publishing the entire repo. No Source Leakage: Only files in /package are seen by consumers—no accidental pushes of TS, docs, or git history. Manual Trigger: The process kicks off only when you create a GitHub Release, not on every push or PR. Scopes, Not Monorepos: No workspaces, no publishing the entire repo. Scopes, Not Monorepos: No Source Leakage: Only files in /package are seen by consumers—no accidental pushes of TS, docs, or git history. No Source Leakage: /package Manual Trigger: The process kicks off only when you create a GitHub Release, not on every push or PR. Manual Trigger: Real-World Example Real-World Example Let’s say you update a Smart Contracts ABI in /src, then run your internal build (maybe via a simple "package" script) to output to /package/dist. /src "package" /package/dist Only that transpiled, dependency-free version ships. that Your API team can safely pull it via: npm install @user/package-name --registry=https://npm.pkg.github.com npm install @user/package-name --registry=https://npm.pkg.github.com From both backend and frontend, with no npmjs exposure.