Learn how to build and publish your own NPM package with Rollup, testing, and troubleshooting. Stay tuned for part 2: building a React state management library!
Akash Deep Chitransh
Last Updated May 26, 2025
Advertisement
Recently, I built and published my own npm package — a minimal state management library for React. It’s tiny, fast, supports selectors for optimal re-renders, and works seamlessly with TypeScript.
My goal wasn't just to create another state library — I wanted to learn the complete journey of publishing a package to npm, from writing the logic to bundling it properly and finally seeing it live on npmjs.com. I also needed a lightweight state manager for my personal projects — something simple and custom-tailored.
In this blog, I’ll walk you through everything I learned while preparing, testing, and publishing a React package to npm.
When I first started, I assumed I could just write some React + TypeScript code and publish it directly to npm. But here's the reality:
NPM packages are meant to be consumed by many different projects, environments, and build systems — so they must be precompiled, optimized, and portable.
Here’s why raw code doesn’t work:
TypeScript Needs Compilation: You can't publish .ts
or .tsx
files directly — they must be compiled to JavaScript.
ES Modules, CJS, or UMD?: Different environments expect different module formats. Some tools prefer ESM (import
), others expect CJS (require
).
Multiple Files & Internal Structure: Consumers shouldn't need to know about your internal file structure — they just need a clean dist/
folder with index.js
, index.d.ts
, etc.
Tree-Shaking and Optimization: To reduce bundle size, your code needs to be treeshakable — meaning your package should only export what’s needed.
That’s where bundlers like Rollup come in — they take your raw TypeScript code and produce a clean, production-ready output.
There are several bundlers out there like Webpack, Vite, and ESBuild. But for publishing libraries, Rollup stands out:
โ Tree-shaking by default — keeps the bundle lean
โ Simpler config for libraries compared to Webpack
โ Great plugin ecosystem — especially for TypeScript and JSX
โ Widely used for React/JS libraries (even used by major open-source projects)
So I chose Rollup to bundle my TypeScript React library into a small, optimized dist/
folder, ready for npm.
To bundle my React + TypeScript library, I used Rollup—a lightweight module bundler perfect for libraries. Here's what my rollup.config.js
looked like:
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import terser from "@rollup/plugin-terser";
export default [
{
input: "src/index.ts",
output: [
{
file: "dist/index.js",
format: "esm",
sourcemap: true,
},
],
plugins: [peerDepsExternal(), typescript(), terser()],
external: ["react", "react-dom"],
},
{
input: "src/index.ts",
output: {
file: "dist/index.d.ts",
format: "es",
},
plugins: [dts()],
},
];
๐ What Each Plugin Does
@rollup/plugin-typescript
: Compiles .ts
and .tsx
files into JavaScript.
rollup-plugin-peer-deps-external
: Automatically marks peerDependencies
(like react
) as external, so they aren't bundled.
@rollup/plugin-terser
: Minifies the final output to reduce bundle size.
rollup-plugin-dts
: Bundles all your .d.ts
files into one clean index.d.ts
for type support.
dist/index.js
: Your bundled library code (ESM format).
dist/index.d.ts
: A single declaration file for TypeScript consumers.
โ Sourcemaps included for better debugging.
package.json
for an NPM LibraryYour package.json
is the blueprint of your library—it tells the world (and npm) how your package works, what it needs, and how to use it. Here's a breakdown of the important fields in mine:
"name": "@akash_deep_chitransh/react-mini-state",
"version": "1.0.3",
"description": "A lightweight and minimal state management solution for small react apps.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"type": "module"
name
: Scoped name (@your-username/package-name
) helps avoid name conflicts.
version
: Semver-compliant version number.
description
: Shows up on npm and GitHub.
main
: Entry point of the library.
types
: Entry point for TypeScript types.
files
: Ensures only the dist
folder gets published.
type: module
: Marks output as ES Modules (used with modern bundlers).
"scripts": {
"build": "rollup -c",
"test": "vitest run",
"test:watch": "vitest"
}
build
: Bundles the code using Rollup.
test
/ test:watch
: Runs unit tests using Vitest.
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
This tells npm:
"Hey, the consuming project must install React—I'll use whatever version they have."
This avoids bundling React into your library, which can lead to duplicate React copies and broken apps.
"devDependencies": {
...
"rollup": "^4.41.1",
"typescript": "^5.8.3",
"vitest": "^3.1.4",
...
}
These are tools used only during development:
Rollup plugins for bundling
TypeScript for typing
Vitest + React Testing Library for unit testing
"repository": {
"type": "git",
"url": "https://github.com/CodeChitra/react-mini-state"
},
"license": "MIT",
"keywords": ["react", "state-management", "store", "typescript"]
Helps developers find and contribute to your library.
Keywords improve discoverability on npm.
Open-source license is a must.
Before pushing your package to the world, it’s critical to test it locally—both through automated unit tests and by consuming it in a real app.
We used Vitest because it’s fast, simple, and works great with TypeScript and React.
npm run test # Run all tests once
npm run test:watch # Re-run tests on file changes
Make sure your test files are placed correctly (__tests__
, .test.ts
, etc.), and your assertions verify your library's core logic (e.g., store creation, state updates, selectors).
npm link
Sometimes unit tests aren't enough. You want to see how the package behaves inside a real React app. That’s where npm link
helps.
npm run build # Ensure the dist folder is ready
npm link # Makes your local package globally linkable
npm link @akash_deep_chitransh/react-mini-state
Now your test app will use the local version of your package—perfect for development and debugging.
Tip: After making changes in your package, just re-run
npm run build
and refresh your app!
Once you're done testing:
npm unlink @akash_deep_chitransh/react-mini-state
npm install # Reinstalls the original dependency if needed
Once I verified everything was working locally, it was time to share my library with the world via npm. Here’s how I did it — along with a few common gotchas you might face too.
package.json
Make sure you’ve set these fields correctly:
{
"name": "@akash_deep_chitransh/react-mini-state",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"license": "MIT",
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
}
name
: Scoped name (@your-username/package-name
) helps avoid conflicts.
files
: Ensures only the dist
folder is published.
main
/types
: Points to the correct entry file and declarations.
npm login
Enter your username, password, and email.
npm publish --access public
Use --access public
if you're publishing a scoped package (@scope/package-name
), or it will fail with a 403.
npm ERR! 403 Forbidden - You do not have permission to publish "package-name"
The package name may already be taken. Try using a scoped name like @your-username/package-name
.
npm ERR! Invalid tag name "^>=18.0.0"
Caused by incorrect peerDependencies
version syntax.
Fix: use valid semver:
โ
"react": ">=18.0.0"
โ "react": "^>=18.0.0"
Once published, your package will be available at:
https://www.npmjs.com/package/@akash_deep_chitransh/react-mini-state
You can install it like this:
npm install @akash_deep_chitransh/react-mini-state
Building and publishing my own NPM package was an incredibly rewarding experience. Not only did I learn how to structure and bundle a reusable library, but I also got hands-on with tools like Rollup, TypeScript, and Vitest—and even tackled real-world issues like dependency management and NPM errors.
Here are the key lessons I took away:
โ
Keep your package.json
clean and precise – especially when it comes to main
, types
, and peerDependencies
.
๐งฉ Bundlers like Rollup help create optimized builds and clean .d.ts
files, critical for library consumers.
๐ Test locally before publishing using npm link
or local installs.
๐ซ Expect some publishing hurdles, especially with permissions and naming. But every error is a learning moment.
This blog was just the first part. In the next one, I’ll dive into how I actually built my own React state management library, including the API design, React hooks, selectors, and how it compares to other tools like Zustand and Redux.
Thanks for reading! ๐
If you found this helpful or are building something similar, let’s connect on LinkedIn.
Advertisement
Advertisement
Advertisement
Alok Kumar Giri
Last Updated May 2, 2025
Code snippet examples which will help to grasp the concept of Hoisting in JavaScript, with solutions to understand how it works behind the scene.
Anuj Sharma
Last Updated Dec 10, 2024
A brief explanation of Cross-Origin Resource Sharing (CORS) concept to enable client application accessing resources from cross domain and HTTP headers involved to enable resource access.
Anuj Sharma
Last Updated Jan 29, 2025
Understand the difference between HTTP/2 vs HTTP/1.1 based on the various parameters, which helps to understand the improvement areas of HTTP/2 over HTTP 1.1
Anuj Sharma
Last Updated Jan 9, 2025
Go through different ways to display dates using javascript date object. It covers examples of date object usage to understand the main concepts of javascript date object.
Anuj Sharma
Last Updated Jan 16, 2025
Deep dive into promise.all polyfill in javascript will help to understand the working of parallel promise calls using Promise.all and its implementation to handle parallel async API calls.
Anuj Sharma
Last Updated Jan 9, 2025
Learn the best & quickest way to format phone number in JavaScript with or without country codes. This will help websites to show the phone numbers in a more human-readable format.
ยฉ 2024 FrontendGeek. All rights reserved