tsnapi: Snapshot Testing for a Package's Public API
Library public API snapshot testing for runtime exports and type declarations.
At a glance
- What is it?
- tsnapi records a library's runtime exports and type declarations into committed snapshot files, so an accidental API change shows up in the git diff. It is a small tool with a narrow job, and it is honest about the checks it does not perform.
- Who is it for?
- Adopt tsnapi if you maintain a published package and want its public surface under review in every pull request, particularly if you already build with tsdown or run Vitest. Skip it if you ship an application rather than a library, or if you cannot commit generated snapshot files and review them.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 16 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The accidental export deletion tsnapi is built to catch
A published library has a contract that is wider than its source files. Rename an export, drop a member from an interface, or change a function signature, and the type checker inside your own repository stays quiet because nothing in the repository used it. The break surfaces later, in someone else's build.
tsnapi targets exactly that gap. The README lists four accidents it is meant to make visible: removing or renaming an export, changing a function signature, breaking type declarations, and introducing unintended public API surface. The audience is library maintainers, not application developers. An application has no external contract to protect, so the snapshots would be noise.
The README frames the idea by comparison: "Think of it like Vitest's snapshot testing, but for your package's public contract." The output is a pair of files per entry point, a .snapshot.js describing runtime exports and a .snapshot.d.ts describing types, both committed to the repository. Review then happens in the same place as every other change.
Two snapshot files per entry point, and what writes them
The mechanism is deliberately boring. tsnapi reads the built output, not the source. The CLI description says it reads package.json exports, then parses the dist files, which is why the README tells you to run the build first so that dist exists. Every integration point sits downstream of that parse.
Three entry points are exposed in package.json: the default export, ./rolldown, ./vitest, plus ./cli for the binary named tsnapi. The Rolldown plugin is described as the recommended route, used through tsdown. The Vitest integration is higher level and stores snapshots as individual files via toMatchFileSnapshot. The CLI covers packages that use neither.
One consequence of reading dist is worth stating plainly: a stale dist produces a snapshot of stale code. The README addresses this by recommending that the test script build first, and points to tsdown-stale-guard for tsdown users. That is a real ordering constraint, not a footnote.
Installing tsnapi and taking a first snapshot
Install it as a development dependency with the package manager the project itself uses.
pnpm add -D tsnapiThe recommended setup is the Rolldown plugin inside a tsdown config. On the first build the plugin writes the snapshot files; on later builds it compares against them and fails the build with a diff when the API changed.
// tsdown.config.ts
import { defineConfig } from 'tsdown'
import ApiSnapshot from 'tsnapi/rolldown'
export default defineConfig({
entry: ['src/index.ts'],
dts: true,
plugins: [
ApiSnapshot()
],
})If you do not use a bundler, the CLI snapshots the current package by reading package.json exports and parsing dist. Build first, then run it.
tsnapiAfter that first run, look for a __snapshots__ directory in the repository and commit the files it contains. On the next build, an unchanged API produces no diff. When you change the API on purpose, update the snapshots with the flag or the environment variable:
tsdown --update-snapshot
# or
UPDATE_SNAPSHOT=1 tsdownThe breaking-change guard is lossy on purpose
Updating snapshots is where the interesting design decision lives. When you run an update, tsnapi classifies the change as additive or breaking, and a breaking change aborts the update without overwriting the snapshot. The error output the README shows names the entry point, the removed export, and the narrowed type, then refuses to continue unless you re-run with --allow-breaking or set TSNAPI_ALLOW_BREAKING=1.
The classifier is asymmetric. Removing an export, removing an interface member, parameter or union arm, or replacing a type with an unrelated one counts as breaking. Adding an export, adding an interface property, widening a parameter with a union, or adding a parameter counts as additive and passes through. The README states the reasoning directly: the check "is deliberately lossy" and favours not blocking a change to avoid false positives.
That trade-off has a visible cost, and the README names it. Widening a return type is a case the guard lets through even though callers may not expect it. So the guard is not a correctness checker for semantic versioning. It is a tripwire for the case the author cares most about, something being removed. The compensating control is that a normal comparison build fails on any change at all, additive included, so nothing slips past review unnoticed.
tsnapi ui, and its read-only rule
The tsnapi ui command opens an interactive graph of a workspace's public API, built on devframe and @antfu/design. The tree runs workspace, package, entry, member, with each member coloured by kind and by diff status: added, removed, modified (narrowed) or widened. The statuses reuse tsnapi's own breaking-change analysis, so the inspector inherits the same asymmetry described above.
The constraint that matters is stated in the README: the inspector only ever reads already-generated __snapshots__ files and never regenerates them. Historical refs come from the committed snapshot via git show. The working-tree side reads whatever snapshot currently sits on disk, uncommitted edits included. If you have not run tsnapi or tsnapi -u first, the graph shows you an old state with no warning.
There is also a static export path. tsnapi ui build --out-dir dist-inspector produces a self-contained build of the current state, and adding --base and --compare bakes a read-only diff between two refs, for example --base v1.1.0 --compare HEAD. That is a sharing mechanism, not a CI gate.
Where tsnapi is the wrong tool, and what to use instead
tsnapi does not run your code. It parses built artifacts, which means it cannot tell you whether the exports behave correctly, only that they exist and have a given shape. Runtime behaviour is still the job of a test suite. If your problem is that a function returns the wrong value, this tool contributes nothing.
It is also a poor fit for applications and for internal packages that are never published, because the public surface of an unpublished package is not a contract anyone depends on. And it needs a build step in the loop. A repository that runs tests directly against source without producing dist cannot use the CLI path as described.
The closest alternative in the same space is an API extractor style tool, which generates a report file from the type declarations of a package. The difference in approach is scope: a declaration report covers types only, while tsnapi produces a runtime snapshot and a type snapshot as a pair, and adds the update-time breaking-change classification. The trade-off runs the other way too. A declaration-only tool never needs a built dist, so it cannot be fooled by stale build output the way tsnapi can.
Licence, release cadence and the cost of upgrading
The package is MIT licensed, which permits commercial use and modification with the licence text retained. That is a statement about the licence file, not legal advice for any particular organisation.
The repository is not archived, and the last push was on 2026-08-24, the same day as the v1.4.0 release. Releases are frequent: v1.2.0 on 2026-07-22, v1.3.0 on 2026-08-18, v1.4.0 on 2026-08-24. Note that package.json in the repository declares version 1.5.0, ahead of the most recent release listed, so the repository state is not identical to the published tag.
The upgrade cost is concentrated in one place: the snapshot files. A tool change that alters snapshot formatting turns every snapshot in every package into a diff, and you will be reviewing those before you review any real API change. That is the price of committing generated files. It is manageable for a repository with one entry point and considerably less pleasant for a monorepo with many. The peer dependency on Vitest is marked optional and requires version 4 or above, so the Vitest integration is the only path that constrains your test runner version.
Editorial conclusion
Adopt tsnapi if you maintain a published package and want its public surface under review in every pull request, particularly if you already build with tsdown or run Vitest. Skip it if you ship an application rather than a library, or if you cannot commit generated snapshot files and review them. Before relying on the guard, verify one thing yourself: that a normal comparison build fails on an additive change, since the breaking-change classifier only runs while updating and deliberately allows additions through.
Frequently asked questions
What is tsnapi used for?
It captures a package's public API surface, both runtime exports and type declarations, into snapshot files that you commit alongside your code. When the API changes unexpectedly, the build fails with a diff instead of the change passing unnoticed.
How do I install tsnapi?
The README gives pnpm add -D tsnapi, installing it as a development dependency. The package also exposes a tsnapi binary, so it can be run from the command line after installation.
How do I update tsnapi snapshots after an intentional API change?
Run the build with the --update-snapshot or -u flag, or set UPDATE_SNAPSHOT=1 in the environment, or set the update option to true on the plugin. If the change removes something, the update aborts and you must re-run with --allow-breaking or set TSNAPI_ALLOW_BREAKING=1.
Does tsnapi work without a bundler?
Yes. The CLI path snapshots the current package by reading package.json exports and parsing the dist files, so you need to run the build first to generate dist. The Rolldown plugin and the Vitest integration are the other two supported routes.
Why does the tsnapi UI show an API state that no longer matches my code?
The inspector only reads already-generated __snapshots__ files and never regenerates them. Run tsnapi or tsnapi -u, or your Vitest snapshot tests, to regenerate a snapshot before inspecting it.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/antfu-tsnapi)