The Hidden UX of Installing a Package
The first five minutes with a new package are a UX experience — install friction, README clarity, error messages. Most library authors never think about it.
Every decision a library author makes about naming, peer dependencies, TypeScript types, and error messages affects the developer who installs the package for the first time. That first five minutes — npm install, open editor, write first import, hit first error — is a UX surface. Most library authors optimize for the demo and forget about install.
The npm install output is the first impression
Run npm install some-library and watch the output. If it prints peer dependency warnings, that is a bad first impression before you have written a single line of code:
npm warn peer dep missing: react@>=18, required by some-library@2.1.0
npm warn peer dep missing: react-dom@>=18, required by some-library@2.1.0
Even if the warnings are expected (the user has React 17), seeing warnings on install creates doubt. Did I install the wrong version? Does this actually work with my setup? Is something broken?
Contrast with a library that installs cleanly, no output except the package name and version. Silence is confidence.
The fix for library authors: Accurate peer dependency ranges. If your library works with React 17 and 18, say so. If it only works with React 18, say that clearly in peerDependencies and in the README's requirements section, so users find out before they install.
The first import
After install, the user writes their first import. Three things can go wrong:
The package name does not match the import path. Installing some-library but importing from SomeLibrary (yes, this exists) breaks muscle memory and autocomplete.
TypeScript types are not automatically available. If types require a separate @types/ install that is not mentioned until the user sees a red squiggle, that is a gap. Modern packages should ship types in the main package. The types field in exports handles this correctly.
The import is deep and undiscoverable. If the user has to know to write import { Button } from 'some-library/components/button/index' rather than import { Button } from 'some-library', something is wrong with the exports map.
A good example: import { useQuery } from '@tanstack/react-query' — one import, correctly typed immediately, autocomplete works, no separate type install needed. TanStack Query gets this right in a way most libraries do not.
A bad example (a library I will not name): import DatePicker from 'react-datepicker' — works, but then you need to separately import import 'react-datepicker/dist/react-datepicker.css' which is not in the getting-started snippet, and the CSS path breaks if you switch bundlers. The first error the user sees is about missing styles, which looks like a bug, not a configuration step.
The first error message
What happens when the user misuses the API? If queryKey is required and they forget it:
Bad error message:
TypeError: Cannot read properties of undefined (reading 'forEach')
at QueryObserver.updateResult (query-observer.js:142)
This is a library internal crashing on undefined input with no indication of what the user did wrong.
Good error message:
Error: queryKey is required and must be an array.
Received: undefined
If you're trying to use a dynamic key, ensure it's always an array:
useQuery({ queryKey: ['user', userId], queryFn: fetchUser })
The difference is an input validation check at the library's boundary with a human-readable message. This costs the library author 3 lines of code and saves every new user 10 minutes of confusion. A good error object is a UX pattern — the shape you return when things go wrong is as much a design decision as the shape you return when they succeed.
Bundle impact on first use
The first time the developer opens a bundle analyzer (or looks at Lighthouse), they see your library's footprint. Libraries that import large transitive dependencies create a bad impression that is difficult to undo, even if the feature is worth the size.
date-fns solved this by providing individual function imports and a well-structured exports map — you pay only for what you use. A comparable date library that ships a 70kb bundle on import { format } from 'some-dates' is going to show up in performance reviews and create pressure to remove it.
The developer's first bundle analysis is a UX moment. Ship a package with sideEffects: false, subpath exports, and no surprise transitive dependencies. Be the library that someone is happy to show up in their bundle, not the one they apologize for.
The install experience as product thinking
None of this is technically difficult. Clean peer deps, a proper exports map, types in the package, helpful error messages, a reasonable bundle footprint. What is missing is thinking about the install experience as a product surface with a user journey, not a technical artifact. The package.json is a product surface — every field is a decision that affects the developer who installs your library. The developer who installs your package has a workflow, expectations, and limited patience. Every friction point in the first five minutes is friction that competes with the library actually doing its job.