DEV Community

Karthik Gs
Karthik Gs

Posted on

# I Just Published My First npm Package — Here's Everything I Did

A complete walkthrough of publishing Cartlify — a React e-commerce UI kit — to npm for the first time.


The Milestone

Yesterday I published Cartlify to npm.

npm install cartlify
Enter fullscreen mode Exit fullscreen mode

It sounds simple. But getting to that one line took more decisions, more configuration, and more trial and error than I expected.

This article covers everything — from setting up the build config to the actual publish command — so you don't have to figure it out the hard way.


What Is Cartlify?

Cartlify is a production-ready React + TypeScript + Tailwind CSS component library focused on e-commerce UI.

4 components that every e-commerce project needs:

  • ProductCard — 3 layout variants, image gallery, wishlist, sale badges, skeleton loading
  • CartDrawer — animated slide-in, focus trap, ESC dismiss, quantity stepper
  • CheckoutStepper — horizontal/vertical, animated connectors, keyboard navigation
  • PageLoader — 4 animation styles, 3 position modes

Plus 3 utility hooks, 11 tree-shakeable icons, 40+ CSS design tokens, full dark mode, and 141 Jest + React Testing Library tests.

Built so freelance developers and indie makers can skip the painful e-commerce UI layer and ship faster.


Why Publish to npm?

Before npm, Cartlify was only available on Gumroad as a paid download.

That's fine — but npm adds something Gumroad can't:

Developer sees Cartlify →
runs npm install cartlify →
evaluates the compiled output →
trusts the quality →
buys the full source on Gumroad
Enter fullscreen mode Exit fullscreen mode

npm is a credibility and discovery channel — not just a distribution method. A package on npm signals that something is real, maintained, and production-ready.

Also: npmjs.com gets millions of developer searches every month. That's free traffic you can't get from Gumroad alone.


The Build Setup — tsup

The most important decision before publishing is how you bundle your library.

I chose tsup — a zero-config TypeScript bundler built on esbuild. Here's why:

Tool Config needed Speed Output
Rollup Lots Medium ESM + CJS
Webpack Heavy Slow CJS only
Vite lib mode Some Fast ESM + CJS
tsup Almost zero Very fast ESM + CJS + .d.ts

My tsup.config.ts:

import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  sourcemap: false,
  clean: true,
  minify: true,
  external: ['react', 'react-dom'],
  esbuildOptions(options) {
    options.alias = {
      '@components': './src/components',
      '@primitives': './src/primitives',
      '@hooks': './src/hooks',
      '@utils': './src/utils',
      '@types': './src/types',
    };
  },
});
Enter fullscreen mode Exit fullscreen mode

Key decisions:

  • format: ['cjs', 'esm'] — supports both older and modern bundlers
  • dts: true — generates .d.ts TypeScript type definitions
  • minify: true — compiled output is minified (buyers get clean source on Gumroad)
  • sourcemap: false — no source maps in public npm package
  • external: ['react', 'react-dom'] — don't bundle React itself

After running npm run build, the dist/ folder looks like:

dist/
├── index.js      ← CommonJS
├── index.mjs     ← ES Module
└── index.d.ts    ← TypeScript types
Enter fullscreen mode Exit fullscreen mode

The package.json Setup

This is the most critical file for npm publishing. Every field matters.

{
  "name": "cartlify",
  "version": "1.0.0",
  "description": "Production-ready React e-commerce UI kit — ProductCard, CartDrawer, CheckoutStepper, PageLoader. Built with TypeScript, Tailwind CSS and Storybook.",
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "files": [
    "dist",
    "README.md"
  ],
  "scripts": {
    "build": "tsup src/index.ts",
    "prepublishOnly": "npm run build && npm run lint && npm run test"
  },
  "keywords": [
    "react",
    "typescript",
    "tailwind",
    "ui-kit",
    "ecommerce",
    "cart",
    "product-card",
    "checkout",
    "storybook",
    "component-library",
    "frontend"
  ],
  "author": "Karthik G S <karthikgs.softengg@gmail.com>",
  "license": "MIT",
  "homepage": "https://cartlify.vercel.app",
  "repository": {
    "type": "git",
    "url": "https://github.com/thirumalai77/cartlify"
  },
  "bugs": {
    "url": "https://github.com/thirumalai77/cartlify/issues"
  },
  "peerDependencies": {
    "react": ">=18.0.0",
    "react-dom": ">=18.0.0"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Key fields to get right:

files — only publish what buyers need:

"files": ["dist", "README.md"]
Enter fullscreen mode Exit fullscreen mode

This stops src/, .storybook/, node_modules/, and test files from being included in the npm package.

exports — modern bundler resolution:

"exports": {
  ".": {
    "import": "./dist/index.mjs",
    "require": "./dist/index.js",
    "types": "./dist/index.d.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

peerDependencies — don't bundle React:

"peerDependencies": {
  "react": ">=18.0.0",
  "react-dom": ">=18.0.0"
}
Enter fullscreen mode Exit fullscreen mode

prepublishOnly — runs before every publish automatically:

"prepublishOnly": "npm run build && npm run lint && npm run test"
Enter fullscreen mode Exit fullscreen mode

This saved me from accidentally publishing broken code.


The .npmignore File

Create this in your root to stop unnecessary files from being published:

src/
.storybook/
*.stories.tsx
*.test.tsx
*.test.ts
node_modules/
.eslintrc.json
.prettierrc
tsconfig.json
tailwind.config.js
tsup.config.ts
coverage/
.github/
Enter fullscreen mode Exit fullscreen mode

Without .npmignore, npm would publish your entire project including source code, test files, and config — making your package unnecessarily large.


Path Aliases — The Tricky Part

Cartlify uses path aliases to avoid ../../ imports:

import { Button } from '@primitives/Button';
import { cn } from '@utils/cn';
import type { Product } from '@types';
Enter fullscreen mode Exit fullscreen mode

This works fine in development. But when you build for npm, the compiled output still has the alias references — and they break for the end user.

The fix is in tsup.config.ts:

esbuildOptions(options) {
  options.alias = {
    '@components': './src/components',
    '@primitives': './src/primitives',
    '@hooks': './src/hooks',
    '@utils': './src/utils',
    '@types': './src/types',
  };
},
Enter fullscreen mode Exit fullscreen mode

This tells tsup to resolve aliases during compilation — the output dist/ files have no aliases, just clean relative paths.


Creating the npm Account

  1. Go to npmjs.com → Sign Up
  2. Verify your email
  3. Enable 2FA — npm now requires this for publishing
  4. Login in terminal:
npm login
Enter fullscreen mode Exit fullscreen mode

It opens a browser for 2FA confirmation. Once verified:

npm whoami
# → yourusername
Enter fullscreen mode Exit fullscreen mode

The Dry Run — Always Do This First

Before publishing for real:

npm publish --dry-run
Enter fullscreen mode Exit fullscreen mode

This shows exactly what will be uploaded without actually publishing:

npm notice 📦  cartlify@1.0.0
npm notice === Tarball Contents ===
npm notice 2.1kB  README.md
npm notice 18.4kB dist/index.js
npm notice 16.2kB dist/index.mjs
npm notice 8.9kB  dist/index.d.ts
npm notice === Tarball Details ===
npm notice name:          cartlify
npm notice version:       1.0.0
npm notice filename:      cartlify-1.0.0.tgz
npm notice package size:  12.3 kB
npm notice unpacked size: 45.6 kB
npm notice total files:   4
Enter fullscreen mode Exit fullscreen mode

Check two things:

  • Only dist/ and README.md are listed ✅
  • No source files, test files, or config files ✅

The Publish Command

npm publish --access public
Enter fullscreen mode Exit fullscreen mode

Output:

npm notice Publishing to https://registry.npmjs.org/
+ cartlify@1.0.0
Enter fullscreen mode Exit fullscreen mode

That's it. Live at npmjs.com/package/cartlify.


Adding npm Badges to README

After publishing, add these to your README.md:

[![npm version](https://badge.fury.io/js/cartlify.svg)](https://npmjs.com/package/cartlify)
[![npm downloads](https://img.shields.io/npm/dm/cartlify.svg)](https://npmjs.com/package/cartlify)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Enter fullscreen mode Exit fullscreen mode

They render as clickable badges on GitHub — adds credibility instantly.


The Free npm + Paid Source Model

Cartlify uses a split model:

npm install cartlify (free)
→ Compiled dist/ output
→ ESM + CJS + TypeScript types
→ All 4 components usable

Gumroad ($29 one-time)
→ Full TypeScript source code
→ Storybook documentation
→ All hooks, icons, design tokens
→ README + usage examples
→ Future updates
Enter fullscreen mode Exit fullscreen mode

The npm package gives developers everything they need to use Cartlify in a project. The Gumroad source lets them read, modify, and fully own the code.

This is the same model used by shadcn/ui, Tailwind UI, and most successful component libraries — free to use, paid to own fully.


What I Learned

1. tsup is the right tool for libraries.
Zero config, fast, outputs exactly what npm needs. Don't overthink the build setup.

2. prepublishOnly is your safety net.
It runs your build, lint, and tests automatically before every publish. You can't accidentally publish broken code.

3. Path aliases need special handling.
They work in development but break in compiled output without the esbuildOptions.alias config.

4. Always dry run first.
npm publish --dry-run saves you from publishing the wrong files. Run it every time.

5. files in package.json matters.
Without it, npm publishes your entire project. Specify exactly what should ship.

6. npm is a credibility signal.
A package on npm feels more legitimate than a Gumroad ZIP alone. Developers trust it more.


Try Cartlify

npm install cartlify
Enter fullscreen mode Exit fullscreen mode

🔗 npmnpmjs.com/package/cartlify

🔗 GitHubgithub.com/thirumalai77/cartlify

🔗 Live Storybookcartlify.vercel.app

🛒 Full source on Gumroad ($29)karthiksoftengg.gumroad.com/l/cartlify-react-ui-kit


What's Next

Now that Cartlify is on npm, next steps are:

  • Product Hunt launch
  • Grow npm downloads and Gumroad sales
  • Write more tutorials around the components

Will post a 30-day update with real numbers — downloads, views, and sales.

If you've published an npm package before — what do you wish you'd known? Drop it in the comments.


Built by Karthik G S — Senior Frontend Engineer with 10+ years in React, TypeScript, and React Native. Building Cartlify as an indie product alongside a full-time role.


Tags: #react #typescript #npm #webdev #javascript #tutorial #showdev #indiehacker

Top comments (0)