Engineering
Lessons Learned Building a TypeScript Library
What broke when I published @reetesh/sudoku-engine and lextrix on npm — exports, semver, types, READMEs, and the gap between publish and maintenance.
Publishing a TypeScript library on npm looks straightforward until you do it twice and realise the first time was luck.
I have two public packages that taught me different lessons: @reetesh/sudoku-engine for the Sudoku game on this site, and lextrix for the editor ecosystem I have been building for longer than I planned. Same toolchain on paper — TypeScript, npm, GitHub Actions — completely different failure modes.
This post is what I wish someone had told me before my first publish. Not generic "semantic versioning good" advice. Actual things that broke.
Lesson 1: Your first API shape will be wrong
The Sudoku engine started as functions inside a React hook. generatePuzzle(), validateMove(), solve() — all exported from one file because the game was the product, not the library.
When I extracted it to npm, I renamed things for "cleanliness." Consumers (well, me, two weeks later) had to rewrite imports. Nobody else was using it yet, so the damage was zero. That will not be true forever.
What I do now: keep the npm package API boring and stable. Internal names can change behind explicit exports. If I need a breaking change, I bump major and write a one-line migration note in the README. No silent renames.
For Lextrix, the package split forced this early. lextrix-change cannot casually rename compose without breaking anyone building undo on top. Smaller surface area per package helps — but multiplies release coordination pain. Tradeoff, not a win.
Lesson 2: exports in package.json is not optional anymore
Modern Node, bundlers, and tools expect the exports field. I learned this when a Next.js app resolved the wrong entry and I got Cannot use import statement outside a module in a server component path that should never have touched the editor bundle.
Rough pattern that worked:
{
"name": "lextrix",
"type": "module",
"main": "./dist/lextrix.umd.cjs",
"module": "./dist/lextrix.js",
"types": "./dist/lextrix.d.ts",
"exports": {
".": {
"types": "./dist/lextrix.d.ts",
"import": "./dist/lextrix.js",
"require": "./dist/lextrix.umd.cjs"
},
"./style.css": "./dist/lextrix.css"
}
}Your exact paths will differ. The lesson is not copy-paste this JSON. The lesson is: test resolution paths you do not use daily — require(), dynamic import(), CSS side effects, subpath exports for /change or /dom if you split packages.
Scoped packages (@reetesh/sudoku-engine) add another footgun: people forget the scope when installing. Document the install command like you are talking to tired-you at midnight.
Lesson 3: Types are part of the public API
I shipped a minor version where a generic defaulted differently. TypeScript consumers broke without a runtime change. Semver purists argue whether that is a major bump. Pain is pain.
Rule I follow now: if TS types change in a way that forces consumer code edits, treat it as breaking even if JavaScript still runs.
Also generate .d.ts from the same build pipeline as JS. Hand-maintained types drift. I have seen projects where types describe an API that shipped two versions ago. Embarrassing and hard to debug.
Lesson 4: Build output should be boring
I tried fancy for a week — multiple bundlers, experimental configs, output formats I did not need. Build flakiness ate the time I saved on the hello-world demo.
Current bias:
- One primary build tool per repo (esbuild or tsup-style setup for libraries)
- Deterministic output directory
- CI runs build + test on every PR
- Published tarball contains only
dist/and README — not source, not tests, not my.vscodefolder (yes, I almost published that once)
For Lextrix monorepo, coordinating builds across packages is its own job. Root scripts that build in dependency order saved me. "Build package X alone" failing because package Y was stale is a full afternoon gone.
Lesson 5: Tests that only run in your head do not count
Sudoku engine: solver correctness is testable with fixtures — puzzles in, solutions or failure out. Easy wins. Property-based tests would be better; I still have that on the list.
Lextrix: pure change-layer functions test well in Node. DOM-linked behaviour does not. I leaned on unit tests for operations and manual browser clicking for selection quirks. That gap shows up as regressions when I refactor blots.
Lesson: split pure logic early even if it feels like over-engineering. The editor shell will never be fully testable in Vitest alone, but invert(compose(a, b)) can be.
Lesson 6: README is the homepage
GitHub README and npm page are most visitors' entire experience. My early READMEs listed features like marketing bullets. Useless.
What works better:
- Install command
- Minimal working example (copy-paste runnable)
- Link to docs / monorepo path
- Explicit "not ready yet" list for roadmap items
- License
For Lextrix I added the install one-liner and links to docs tree on GitHub. Still need a hosted playground — README cannot compensate for try-it-yourself forever.
Lesson 7: Version numbers are communications
I bumped patch versions for internal refactors consumers should not have noticed. I delayed major bumps because I did not want to look "unstable." Both choices confused future-me.
Now:
- Patch — bug fix, no API change
- Minor — backward-compatible feature
- Major — break or intentional API cleanup
Pre-1.0 (0.x) is not a license to break weekly without changelog notes. People still depend on 0.x in production. Ask npm download stats if you doubt that.
Keep a CHANGELOG even if it is ugly. "Fixed invert bug with nested list items" helps more than fix: stuff.
Lesson 8: Dependencies are a liability
Sudoku engine: zero runtime dependencies. Beautiful. Small tarball. Fast installs.
Lextrix: intentionally few, but build-time tooling adds up. Every devDependency is a supply-chain node. Not paranoia — maintenance.
Before adding a dependency I ask:
- Can I write 40 lines instead? (Sometimes yes, often no)
- Will this run in consumer browsers or only at build time?
- Who maintains it when ESLint 27 breaks the plugin ecosystem again?
Lesson 9: Publishing is not the finish line
First npm publish felt huge. Download count stayed at zero for days. Normal.
Real work after publish:
- Issue triage
- Repro steps from strangers who use Windows and a browser you forgot to test
- Documentation PRs you should have written yourself
- Deciding whether feature requests align with scope
Open source is a long feedback loop. Sudoku engine gets occasional installs. Lextrix gets fewer but deeper issues when someone actually embeds it.
Lesson 10: Dogfood or drift
Both libraries exist because this site uses them. Sudoku game imports the engine. Lextrix dogfooding is slower — migrating this blog to Lextrix MDX authoring is on my list, not done yet. That gap matters. Without dogfooding, API pain stays theoretical.
If you publish a library you do not use, expect API drift toward what you imagine people want instead of what integration requires.
India-specific practical note (not a sob story)
I build this from India on normal hardware, evenings and weekends, timezone overlap with US/EU OSS communities limited. Release notes go out when I am awake; issues sit overnight. That is fine. Async OSS works if you document response times honestly in README.
Do not pretend to be a funded startup. Solo maintainers set expectations with tone: "I maintain this in spare time" is not weakness; it prevents resentment on both sides.
What I would tell past-me
- Ship
@reetesh/sudoku-engineearlier — smaller scope, faster learning loop - Split Lextrix packages before the god object hardened
- Write the Quill reading notes before writing change-layer abstractions
- Publish changelogs from commit one
- Stop waiting for perfect docs to npm publish — but do not leave docs empty forever either
CI and release automation (the unglamorous part)
GitHub Actions for test + build on push is table stakes. What I under-invested in early: automated npm publish on tag with provenance and a dry-run check of package contents (npm pack + inspect tarball). I published a broken tarball once because files in package.json was wrong. Nobody downloaded it. Lucky.
Now I check tarball contents before publish. Boring step. Prevents public oops.
For monorepos, version bump coordination across packages is still manual-ish. Tools exist (changesets, etc.). I have not adopted one fully yet — another item on the honest roadmap list.
If you are publishing your first TS library
Pick a scope so small it feels embarrassing. Sudoku validation without generation. One Lextrix package without themes. Get install + import working on a fresh Vite app and a fresh Next app. Then expand.
Authority comes from maintained, usable packages — not from fifty abandoned @scope/useful-utils repos.
These lessons cost me weekends. Maybe they save you one afternoon. If you publish something and hit a weird exports resolution bug, open an issue somewhere — someone else probably hit it too and wrote nothing about it.
That was me for too long.