Stack Dev Life
How to Create and Publish Your First npm Package (and Use It in Angular)
Back to Blog

How to Create and Publish Your First npm Package (and Use It in Angular)

This guide explains how to convert a TypeScript utility into a reusable npm package and use it in an Angular app. It covers project setup, configuration, publishing, and how to install, import, and run it in Angular, including common issues and solutions.

SB

Sandeep Bansod

April 2, 20265 min read
Share:
How to Create and Publish Your First npm Package (and Use It in Angular)

Have you ever written a small utility function and thought:"This could be useful to others too!"Good news—you can package it up, publish it on npm, and then use it anywhere (even in an Angular app). Don’t worry, it’s much simpler than it sounds. Let’s go step by step.

Prerequisites

  1. Node.js 18+ and npm 9+ (node -v, npm -v)
  2. An npm account: https://www.npmjs.com/signup
  3. Optional but recommended: enable 2FA on npm (Account → Settings → “Enable 2FA”)


What we’ll build

A tiny TypeScript utility package called @your-scope/handy-utils with a couple of functions:

  • slugify(text: string): string
  • maskPhone(phone: string): string

We’ll:

  1. scaffold a package,
  2. write & build TypeScript,
  3. publish to npm,
  4. install and use it in an Angular application.

1) Create the package project

TEXT
mkdir handy-utils && cd handy-utils
git init
npm init -y

This creates a basic package.json. We’ll turn it into a modern TS package.

Install dev dependencies

npm i -D typescript @types/node rimraf

Add project structure

handy-utils/
├─ src/
│  └─ index.ts
├─ tsconfig.json
├─ .npmignore   (optional; we’ll prefer `"files"` in package.json)
├─ README.md
└─ LICENSE

tsconfig.json

JSON
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "node",
    "declaration": true,
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

src/index.ts

TypeScript
export function slugify(input: string): string {
  return String(input)
    .trim()
    .toLowerCase()
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

export function maskPhone(phone: string): string {
  const digits = String(phone).replace(/\D/g, "");
  if (digits.length <= 4) return digits;
  const masked = "*".repeat(Math.max(0, digits.length - 4)) + digits.slice(-4);
  return masked;
}

2) Prepare package.json for modern publishing

Open package.json and update important fields:

JSON
{
  "name": "@your-scope/handy-utils",
  "version": "1.0.0",
  "description": "Tiny TypeScript utilities: slugify & maskPhone",
  "keywords": ["utils", "slugify", "phone", "typescript"],
  "license": "MIT",
  "author": "Your Name <you@example.com>",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "scripts": {
    "clean": "rimraf dist",
    "build": "npm run clean && tsc && node ./scripts/cjs-build.js",
    "prepublishOnly": "npm run build"
  }
}

Why both ESM and CJS? Angular and many modern tools work best with ESM, but some environments still require CommonJS. We’ll convert ESM output to CJS with a tiny script.

Create scripts/cjs-build.js:

TypeScript
// Simple CJS re-exporter for Node require() users
// Creates dist/index.cjs that proxies to the ESM build.
import { writeFileSync } from "node:fs";
const content = `module.exports = require('./index.js');`;
writeFileSync("dist/index.cjs", content);

Add the folder:

BATCHFILE
mkdir scripts
# ...then paste the file above into scripts/cjs-build.js

3) Add a README and LICENSE

README.md (short example)

JavaScript
# @your-scope/handy-utils

Tiny utilities in TypeScript: `slugify`, `maskPhone`.

## Install
```bash
npm i @your-scope/handy-utils

4) Build locally

BATCHFILE
npm run build

5) Log in to npm & choose your package name

If this is a scoped package (starts with @your-scope/), and you want it public, you must publish with --access public the first time.

BATCHFILE
npm login
# enter your npm username/password/2FA

Unscoped (non-scoped) first publish:

BATCHFILE
npm publish

If you get 403 or 2FA errors, ensure you’re logged in to the correct account, you own the scope, and 2FA is set to “Authorization and Publishing” (or provide an OTP when prompted).

7) Semantic Versioning (updates later)

  1. 1.0.1 – patch (bug fixes)
  2. 1.1.0 – minor (backward-compatible features)
  3. 2.0.0 – major (breaking changes)

Update code, bump version in package.json, then:

BATCHFILE
npm run build
npm publish

8) Use it in an Angular application

Tested with Angular 13+, but works similarly in newer versions.

8.1 Install the package

In your Angular workspace root:

BATCHFILE
npm i @your-scope/handy-utils

8.2 Import and use in a service or component

Example: src/app/services/string-tools.service.ts

TypeScript
import { Injectable } from '@angular/core';
import { slugify, maskPhone } from '@your-scope/handy-utils';

@Injectable({ providedIn: 'root' })
export class StringToolsService {
  toSlug(input: string): string {
    return slugify(input);
  }
  mask(phone: string): string {
    return maskPhone(phone);
  }
}

Example: src/app/app.component.ts

TypeScript
import { Component } from '@angular/core';
import { StringToolsService } from './services/string-tools.service';

@Component({
  selector: 'app-root',
  template: `
    <div class="p-3">
      <h1>Handy Utils Demo</h1>
      <p>Slug: {{ slug }}</p>
      <p>Masked: {{ masked }}</p>
    </div>
  `,
})
export class AppComponent {
  slug = '';
  masked = '';

  constructor(svc: StringToolsService) {
    this.slug = svc.toSlug('Hello Angular World!');
    this.masked = svc.mask('+226 9284733802');
  }
}

Run the app:

BATCHFILE
npm start
# or
ng serve

You should see the slugified and masked values in your UI.

9) Common pitfalls & fixes

  • 403 Forbidden on publish
  • Not logged in (npm whoami to check).
  • Scope ownership mismatch (you must own @your-scope).
  • Missing --access public for first publish of scoped public package.
  • 2FA required: enter OTP when prompted.
  • Name already taken
  • Use a different name or a scope: @your-scope/handy-utils.
  • Esm/Cjs interop issues
  • We publish ESM (dist/index.js) with a small CJS proxy (dist/index.cjs) to support require. Angular uses ESM just fine.
  • Files too big / secrets included
  • Rely on "files": ["dist"] in package.json to publish only build artifacts.
  • Don’t publish .env, test fixtures, etc.
  • Wrong registry
  • Ensure npm config get registry returns https://registry.npmjs.org/.
  • If not: npm config set registry https://registry.npmjs.org/.

10) Nice extras (optional)

  • Automate releases with conventional commits + semantic-release.
  • CI: Run npm ci && npm test && npm run build on push.
  • Changelog: auto-generate via commit messages.
  • Typedoc for API docs.
  • Vitest/Jest for tests.

Final checklist

  • package.json has proper name, version, description, license, exports, types, files.
  • tsconfig.json compiles to dist/ with .d.ts declarations.
  • README.md shows install & usage.
  • npm run build produces dist/ (esm + cjs proxy).
  • npm publish (or npm publish --access public for first scoped publish).
  • Install in Angular and import your functions.
Tags:NPMTYPE SCRIPTPACKAGE
SB

Sandeep Bansod

I'm a Front‑End Developer located in India focused on website look great, work fast and perform well with a seamless user experience. Over the years I worked across different areas of digital design, web development, email design, app UI/UX and developemnt.

Stay in the loop

Get articles on technology, health, and lifestyle delivered to your inbox.No spam — unsubscribe anytime.