
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.

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
- Node.js 18+ and npm 9+ (
node -v,npm -v) - An npm account: https://www.npmjs.com/signup
- 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): stringmaskPhone(phone: string): string
We’ll:
- scaffold a package,
- write & build TypeScript,
- publish to npm,
- install and use it in an Angular application.
1) Create the package project
mkdir handy-utils && cd handy-utils
git init
npm init -yThis creates a basic package.json. We’ll turn it into a modern TS package.
Install dev dependencies
npm i -D typescript @types/node rimrafAdd project structure
handy-utils/
├─ src/
│ └─ index.ts
├─ tsconfig.json
├─ .npmignore (optional; we’ll prefer `"files"` in package.json)
├─ README.md
└─ LICENSEtsconfig.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
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:
{
"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:
// 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:
mkdir scripts
# ...then paste the file above into scripts/cjs-build.js3) Add a README and LICENSE
README.md (short example)
# @your-scope/handy-utils
Tiny utilities in TypeScript: `slugify`, `maskPhone`.
## Install
```bash
npm i @your-scope/handy-utils4) Build locally
npm run build5) 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.
npm login
# enter your npm username/password/2FAUnscoped (non-scoped) first publish:
npm publishIf 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.0.1– patch (bug fixes)1.1.0– minor (backward-compatible features)2.0.0– major (breaking changes)
Update code, bump version in package.json, then:
npm run build
npm publish8) 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:
npm i @your-scope/handy-utils8.2 Import and use in a service or component
Example: src/app/services/string-tools.service.ts
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
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:
npm start
# or
ng serveYou should see the slugified and masked values in your UI.
9) Common pitfalls & fixes
403 Forbiddenon publish- Not logged in (
npm whoamito check). - Scope ownership mismatch (you must own
@your-scope). - Missing
--access publicfor 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 supportrequire. Angular uses ESM just fine. - Files too big / secrets included
- Rely on
"files": ["dist"]inpackage.jsonto publish only build artifacts. - Don’t publish
.env, test fixtures, etc. - Wrong registry
- Ensure
npm config get registryreturns https://registry.npmjs.org/. - If not:
npm config set registryhttps://registry.npmjs.org/.
10) Nice extras (optional)
- Automate releases with conventional commits + semantic-release.
- CI: Run
npm ci && npm test && npm run buildon push. - Changelog: auto-generate via commit messages.
- Typedoc for API docs.
- Vitest/Jest for tests.
Final checklist
package.jsonhas propername,version,description,license,exports,types,files.tsconfig.jsoncompiles todist/with.d.tsdeclarations.- README.md shows install & usage.
npm run buildproducesdist/(esm + cjs proxy).npm publish(ornpm publish --access publicfor first scoped publish).- Install in Angular and import your functions.
Stay in the loop
Get articles on technology, health, and lifestyle delivered to your inbox.
No spam — unsubscribe anytime.