AI-assisted apps often look complete because the successful request is complete. The list renders, the button submits, and the demo ends. The first slow response, empty database, failed request, or double-click tells a different story.
The difficult part is that these gaps are easy to miss in review. You can inspect a component and see a valid query without noticing that its loading and error states are absent. You can run a happy-path demo without ever seeing the blank screen that a real user will see.
This tutorial uses unhappypath, a deterministic CLI for scanning React and Next.js source code for missing unhappy-path UI. It reads files and the TypeScript/JSX AST. It does not run your app, call an LLM, use the network during a scan, or claim that your UX copy is good.
TL;DR
Clone the repository, build the CLI, scan a project, and add a minimum score to CI. The result is a repeatable score from 0 to 100 plus findings such as missing route boundaries, query error branches, async button feedback, and form pending states.
Prerequisites
- Node.js 20 or newer
- npm
- A React or Next.js project on disk
- Git, if you install from the public repository
The repository currently documents version 0.1.0 in package.json. At the time of writing, there is no npm package or GitHub release, so use the source checkout below. This keeps the tutorial aligned with the current public main branch instead of implying that npx unhappypath is already available from npm.
Install from source
The documented source workflow is short:
git clone https://github.com/paladini/unhappypath.git
cd unhappypath
npm ci
npm run build
The build emits dist/cli.js. You can scan any project by passing its root directory:
node dist/cli.js /path/to/your-next-app
On Windows PowerShell, use a quoted path when it contains spaces:
node dist/cli.js "C:\work\billing-dashboard"
The scanner does not need to start the application. It inspects the source tree, so it is useful before a browser test or as a fast local check before opening a pull request.
Run the built-in verification
The repository includes two fixtures that make the scoring contract concrete. Run both from the clone:
npm run demo:broken
npm run demo:good
The broken fixture is intentionally missing several states. The current output reports 14 findings and a score of 38/100. The good fixture contains route boundaries and state branches, and reports 0 findings with a score of 100/100.
You can also run the full test command:
npm test
In the current checkout, this builds the project and runs 18 Node test cases. The fixture tests assert the important behavior, including the score bands, finding IDs, output formats, route checks, and CLI exit codes.
Read a finding as a repair queue
Run the scanner against the intentional broken fixture in text mode:
node dist/cli.js fixtures/broken-app
The report groups findings into four dimensions:
- Routes: Next.js App Router loading, error, not-found, and global-error boundaries
- Queries: loading, error, and empty handling for query hooks and effect-based fetches
- Mutations: pending feedback for async actions and empty catch blocks
- Forms: submitting or pending feedback for form controls
Each finding has a stable ID, a file, a line, and a message. For example, QRY-01 means a query body has no loading-state signal, while MUT-02 identifies an async click handler without pending or disabled feedback. The full catalog is in docs/FINDINGS.md.
The recommended repair order is routes, queries, mutations, and then forms. That order starts with boundaries that protect whole route segments and ends with controls that need local interaction feedback.
Fix the smallest useful example
Consider a query that renders only when data exists:
export default function DashboardPage() {
const { data } = useQuery({
queryKey: ["invoices"],
queryFn: fetchInvoices,
});
return (
<ul>
{data?.map((item) => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
This can produce a blank area while the request is pending, after an error, and when the successful response is an empty array. Add explicit branches before rendering the success state:
export default function DashboardPage() {
const { data, isPending, isError, error, refetch } = useQuery({
queryKey: ["invoices"],
queryFn: fetchInvoices,
});
if (isPending) return <div aria-busy="true">Loading invoices...</div>;
if (isError) {
return (
<div role="alert">
<p>{error.message}</p>
<button type="button" onClick={() => refetch()}>Try again</button>
</div>
);
}
if (!data || data.length === 0) {
return <p>No invoices yet. Create your first invoice.</p>;
}
return (
<ul>
{data.map((item) => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
The TanStack Query query guide describes the pending, error, and success states that this pattern makes visible. The exact UI is your product decision. The scanner only checks for signals that suggest the branches exist.
For a Next.js App Router page, add route-level boundaries as documented by the project:
app/
loading.tsx
error.tsx
not-found.tsx
global-error.tsx
dashboard/
page.tsx
An ancestor boundary can cover nested routes. error.tsx must be a client component and should expose a retry action through reset(). A not-found.tsx file is only the presentation side of missing resources, so resource code still needs to call notFound() when appropriate. See the Next.js error handling documentation and loading file convention for the framework behavior.
Add a CI gate
Once the local result is useful, make the score part of the same check that builds your application. The CLI returns exit code 1 when the score is below --min-score:
node dist/cli.js . --min-score 75
The repository also documents a composite GitHub Action. A workflow can pin the action reference and set a threshold:
- uses: paladini/unhappypath@v1
with:
path: .
min-score: "75"
The current action definition invokes npx unhappypath@0.1.0. Because the package is not published yet, verify the action path in your own CI before depending on it. For a source checkout in this repository, the direct node dist/cli.js command is the reproducible path validated above. The GitHub composite action documentation explains the action structure.
Choose the threshold based on your codebase, not on a universal promise. The project describes 75 as a launch-oriented target and 90 as a stronger production UX target, but a score is evidence about detected branches, not proof of runtime quality.
Why this is useful and where it stops
Static analysis is valuable here because the questions are structural: does a route have an error boundary, does a query body mention an error state, and does an async button expose pending feedback? These checks are fast, deterministic, and cheap to repeat.
They are not complete UX testing. unhappypath does not simulate a slow server, inspect whether a retry actually works, judge the wording of an empty state, or replace browser and integration tests. Its v0.1 documentation also calls out blind spots such as child-component state that may not be detected and server components with async data that are not deeply analyzed.
Treat findings as a review queue. After fixing them, run the application and test real network failures, empty data, keyboard interaction, accessibility behavior, and authorization boundaries. Do not interpret a 100 score as a security or production-readiness guarantee.
FAQ
Does it use an LLM?
No. The scan is filesystem and TypeScript/JSX AST analysis. The repository describes it as deterministic, with no LLM, network, or telemetry in the scan path.
Does it support every frontend framework?
No. The documented focus is React and Next.js, especially the Next.js App Router. Vue, Nuxt, SvelteKit, and Remix are listed as future work.
Can I use it with a minimum score in CI?
Yes. Use --min-score <n>. Exit code 0 means the threshold passed, 1 means the score was below it, and 2 indicates a usage error.
Does it prove that an error screen is good?
No. It detects structural signals. Human review and runtime tests still decide whether the experience is understandable and correct.
Takeaway
The practical value of unhappypath is not the number itself. It is the short, repeatable path from "the demo works" to a list of missing states that someone can fix and recheck. Build the source version, scan a real project, fix the highest-impact findings, and keep a threshold in CI that your team can defend.
This article was prepared with AI assistance for research organization and drafting. Commands, repository facts, and validation results were checked against the public project sources listed above.
What is the first unhappy path your current CI would miss: a route boundary, a failed query, a duplicate action, or an empty result?
Top comments (0)