

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
12345# Frontend API Rules67Use this rule when frontend code imports services from `@generated/doughnut-backend-api/sdk.gen`, handles wrapped API responses, or maps backend validation errors.89## API Integration1011- Import services directly from `@generated/doughnut-backend-api/sdk.gen`.12- Use the wrapped response pattern: `{ data, error, request, response }`.13- Follow the API types from generated code.14- Use `apiCallWithLoading` for user-initiated actions that need loading indicators.15- Wrapped calls show loading state and error toasts automatically.16- Non-wrapped calls are silent: no loading state and no error toasts.1718```typescript19import { UserController } from "@generated/doughnut-backend-api/sdk.gen"20import { apiCallWithLoading } from "@/managedApi/clientSetup"2122const { data: newUser, error } = await apiCallWithLoading(() =>23 UserController.createUser({24 body: formData,25 })26)2728const { data: users, error: loadError } = await UserController.getUserProfile()29```3031## Loading indicators3233`apiCallWithLoading` drives two global surfaces mounted from `DoughnutApp.vue`:3435- **Thin loading bar** — shown while any wrapped call is in flight.36- **Whole-UI blocking modal** — shown when an in-flight call passed `{ blockUi: true }`; its message comes from the latest blocking state. Each call pushes/pops a state by id, so concurrent blockers clean up correctly. The modal is a native `<dialog>` opened with `showModal()`, so it lives in the browser top layer and paints above other top-layer modals (e.g. the Refine note dialog) — `z-index` cannot reach the top layer.3738Use `{ blockUi: true, message?: string }` when a user action must finish before the rest of the UI is usable — view transitions that depend on a result, or mutations where partial interaction would confuse state. Show the blocker only after any confirmation step. Do **not** add component-local `LoadingModal` refs; the global modal is the single whole-UI blocker.3940```typescript41const { data, error } = await apiCallWithLoading(42 () => AssimilationController.next({ query: { timezone: timezoneParam() } }),43 { blockUi: true, message: "Loading next note..." }44)45```4647### Cancelable blocking calls4849For safe **read-only** blockers where the user may abandon the wait, pass the literal opt-in `{ blockUi: true, cancelable: true, message?: string }`. That overload returns `CancelableApiResult<T>` (`{ status: "completed"; result }` | `{ status: "cancelled" }`). Narrow on `status` before using `result`; do not treat cancel as an API error.5051Allowed product opt-ins today (note refinement only): **layout generation** (`AI is generating layout...`) and **extraction-preview generation** (`extractNotePreview` / `AI is generating preview...`). Both use this same overload and status narrowing.5253```typescript54const outcome = await apiCallWithLoading(55 (signal) =>56 AiController.generateRefinementSuggestions({57 path: { note: noteId },58 signal,59 }),60 {61 blockUi: true,62 cancelable: true,63 message: "AI is generating layout...",64 }65)6667if (outcome.status === "cancelled") {68 // Domain-local empty/retry UI only — accepted cancel is silent (no toast).69 return70}7172const { data, error } = outcome.result73```7475```typescript76const previewOutcome = await apiCallWithLoading(77 (signal) =>78 AiController.extractNotePreview({79 path: { note: noteId },80 body: layoutSelection,81 signal,82 }),83 {84 blockUi: true,85 cancelable: true,86 message: "AI is generating preview...",87 }88)8990if (previewOutcome.status === "cancelled") {91 return92}9394const { data, error } = previewOutcome.result95```9697Cancel on the global modal is projected only from the **selected blocker's identity-bound** action; cancelling one state does not clear older concurrent blockers. Accepted cancel is silent (no toast / Cancelling interstitial). Browser abort is **client-only** — it does not promise server work stopped. Do **not** opt mutations or irreversible writes into `cancelable: true` (e.g. create-note / `AI is creating note...` stays noncancelable). Do not invent AbortError-name matching, a cancelable `runWithBlockingApiLoading`, or a parallel helper — use this overload only.9899For one continuous blocker across multiple calls, wrap them in `runWithBlockingApiLoading(operation, message)`; inner `apiCallWithLoading` calls keep thin bar + toasts only. That helper remains noncancelable.100101Keep local loading state (inline spinner, disabled control, row-level pending) when the user should stay able to interact with the rest of the page — e.g. search, pagination, single form-field upload, recall prompt internals. These can still call `apiCallWithLoading` without `blockUi`.102103### Blocking classification inventory104105Every whole-UI blocker and long-running note-refinement AI request must be classified as **cancelable**, **intentionally noncancelable**, or **nonblocking**. Persist new sites here when adding `blockUi: true` or `runWithBlockingApiLoading` — do not leave them unclassified.106107**Cancelable allowlist** (only these product opt-ins may use `{ blockUi: true, cancelable: true }`; exclusivity is gated by `frontend/tests/managedApi/cancelableAllowlist.spec.ts`):108109| Operation | Message | Call site | Notes |110| --- | --- | --- | --- |111| Layout generation | `AI is generating layout...` | `NoteRefinement.vue` | Shared cancelable overload |112| Extraction-preview generation | `AI is generating preview...` | `NoteRefinement.vue` | Shared cancelable overload |113114**Intentionally noncancelable** (whole-UI blocker without Cancel — do **not** opt into `cancelable: true` until a safe post-cancel outcome is defined for that site):115116| Operation | Message | Call site | Mechanism | Notes |117| --- | --- | --- | --- | --- |118| Create extracted note | `AI is creating note...` | `NoteRefinement.vue` | `runWithBlockingApiLoading` | Intentional: client-only abort is unsafe for this transactional write (duplicate note / double original-note update risk). |119| Remove refinement content | `AI is removing content...` | `NoteRefinement.vue` | `runWithBlockingApiLoading` | Mutation; nested post-remove layout reload is nonblocking (see below) |120| Assimilate unit | `Assimilating...` | `useAssimilateUnit.ts` | `{ blockUi: true }` | Mutation |121| Load next assimilation note | `Loading next note...` | `useGoToNextAssimilation.ts` | `{ blockUi: true }` | View transition |122| Book layout mutations | `Updating book layout…` | `useBookLayoutMutations.ts` | `{ blockUi: true }` (2 sites) | Mutation |123| Book layout AI suggest | `Analyzing book layout…` | `useBookLayoutAiReorganize.ts` | `{ blockUi: true }` | Long-running AI read; leave noncancelable until post-cancel UX is defined |124| Book layout apply suggestion | `Applying layout changes…` | `useBookLayoutAiReorganize.ts` | `{ blockUi: true }` | Mutation |125| Note delete / reduce | `Deleting note...` / `Reducing to source property...` | `useNoteDeleteFlow.ts` | `runWithBlockingApiLoading` | Mutation |126| Relationship finalize | `Creating relationship note...` | `AddRelationshipFinalize.vue` | `runWithBlockingApiLoading` | Mutation |127| Attach book upload | `Uploading book…` | `NotebookAttachedBookSection.vue` | `runWithBlockingApiLoading` | Mutation |128129**Nonblocking** (out of Cancel UX — thin-bar or direct SDK; no whole-UI modal):130131| Operation | Call site | Notes |132| --- | --- | --- |133| Export extract request | `NoteRefinement.vue` `fetchExtractRequestExport` | Direct SDK; no `apiCallWithLoading` |134| Export refinement layout request | `NoteRefinement.vue` `fetchBreakdownRequestExport` | Direct SDK; no whole-UI block |135| Post-remove layout reload | `NoteRefinement.vue` `loadRefinementLayout({ blockUi: false })` | Thin-bar nested under remove continuous blocker |136| Load book after attach | `NotebookAttachedBookSection.vue` `loadBook` | Thin-bar `apiCallWithLoading` without `blockUi` |137138| Concern | Mechanism |139| --- | --- |140| Thin top loading bar | Any `apiCallWithLoading` call → `LoadingThinBar` with `data-app-busy` |141| Whole-UI modal | `apiCallWithLoading(..., { blockUi: true, message? })` or `runWithBlockingApiLoading` → `LoadingModal` with `data-app-busy` |142| Cancelable whole-UI modal | `apiCallWithLoading(..., { blockUi: true, cancelable: true, message? })` → `CancelableApiResult` (status narrowing) |143| Page / section pending data | `ContentLoader` with `data-app-busy` |144| Inline / partial progress | Local component state (not part of the busy contract unless it adds `data-app-busy`) |145| Error toasts | `apiCallWithLoading` (handled in `clientSetup.ts`); accepted cancel is silent |146| Silent background fetch | Direct SDK call without `apiCallWithLoading` |147| E2E wait for unfinished work | `waitUntilAppIsNotBusy()` — waits until no `[data-app-busy]` (see below) |148149### E2E pairing (`data-app-busy` ↔ `waitUntilAppIsNotBusy`)150151Loading UI that means “the app has unfinished work” must mark its root element with `data-app-busy`. Today that is `LoadingThinBar`, `ContentLoader`, and `LoadingModal` (enforced by `frontend/tests/components/commons/AppBusyMarker.contract.spec.ts`). Presentational classes stay presentational.152153| Product | E2E |154| --- | --- |155| Elements with `data-app-busy` | `waitUntilAppIsNotBusy()` from `e2e_test/start/pageBase.ts` |156157After a Cypress action that starts loading work, call `waitUntilAppIsNotBusy()` in the page object before the next assertion that depends on that work finishing. The wait means **busy UI cleared**, not success — prefer a user-visible outcome when the product shows one. Do not use hardcoded sleeps. Network intercepts are optional extras, not the default stand-in. Full E2E notes: `.cursor/rules/e2e-authoring.mdc`.158159## API Return Value Usage160161The global client is configured with `responseStyle: "fields"` and `throwOnError: false`, so services return a wrapped response: `{ data, error, request, response }`.162163```typescript164import { getTokens } from "@generated/doughnut-backend-api/sdk.gen"165166const { data: tokens, error } = await getTokens()167168if (!error) {169 tokens.value = tokens.map((token) => ({170 id: token.id,171 label: token.label,172 }))173}174```175176Key points:1771781. Destructure with meaningful variable names: `const { data: updatedUser, error } = await updateUser(...)`.1792. Check `!error` before using `data`; no separate `data` check is needed.1803. When `error` is undefined, TypeScript guarantees `data` is the expected type.1814. Do not add runtime property checks for required typed properties.1825. Do not add `else if (data)` after checking `error`.1836. Use `apiCallWithLoading` for user actions to enable error toasts and loading state.184185## Validation Errors186187When handling 400 Bad Request validation errors, use `toOpenApiError` to extract field-level errors. The `error` field in API responses is typed as `string` but can be an object at runtime after JSON parsing.188189```typescript190import { apiCallWithLoading } from "@/managedApi/clientSetup"191import { toOpenApiError } from "@/managedApi/openApiError"192193const errors = ref<Record<string, string>>({})194195const { data: updatedUser, error } = await apiCallWithLoading(() =>196 UserController.updateUser({197 path: { user: id },198 body: formData,199 })200)201202if (error) {203 const openApiError = toOpenApiError(error)204 errors.value = openApiError.errors || {}205} else {206 errors.value = {}207 user.value = updatedUser208}209```210211Key points:2122131. Use `toOpenApiError(error)` to convert the runtime error value to `OpenApiError`.2142. `OpenApiError` shape: `{ errors?: Record<string, string>; message?: string }`.2153. The helper safely handles both string and object errors.2164. Extract `errors` for field-level validation errors, usually from 400 responses.2175. `apiCallWithLoading` already shows error toasts; only extract field-level errors for form validation.218
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| nerds-odd-e/doughnut.cursor/rules/general.mdc · 49 | Cursor rules | styledo-not | 49/100 | 14 days ago | |
| nerds-odd-e/doughnut.clinerules/daisyui.md · 49 | Cline rules | setuplint-formatstyleui+1 | 57/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/architecture-decisions.mdc · 49 | Cursor rules | no sections | 16/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/backend-code.mdc · 49 | Cursor rules | styletypesdatabasedo-not | 61/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/backend-testing.mdc · 49 | Cursor rules | buildteststyletesting-strategy+2 | 73/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/cli.mdc · 49 | Cursor rules | setupbuildteststyle+4 | 96/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/db-migration.mdc · 49 | Cursor rules | stylearchdatabasedeployment | 64/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/e2e-authoring.mdc · 49 | Cursor rules | setupteststylearch+3 | 80/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/e2e-ocr.mdc · 49 | Cursor rules | setuptesting-strategydo-not | 46/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/frontend-component.mdc · 49 | Cursor rules | testlint-formatstylearch+2 | 76/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/frontend-storybook.mdc · 49 | Cursor rules | buildteststyletesting-strategy+1 | 69/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/frontend-testing.mdc · 49 | Cursor rules | buildteststyletesting-strategy+2 | 89/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/gsd-coexistence.mdc · 49 | Cursor rules | style | 60/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/linting_formating.mdc · 49 | Cursor rules | testlint-formatstylearch+6 | 88/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/mcp-server.mdc · 49 | Cursor rules | buildtestlint-formatarch+2 | 85/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/planning.mdc · 49 | Cursor rules | teststylearchdo-not+1 | 75/100 | 14 days ago | |
| nerds-odd-e/doughnut.cursor/rules/script.mdc · 49 | Cursor rules | testarch | 58/100 | 14 days ago | |
| nerds-odd-e/doughnutAGENTS.md · 49 | AGENTS.md | no sections | 47/100 | 14 days ago | |
| nerds-odd-e/doughnutCLAUDE.md · 49 | CLAUDE.md | agent-behaviour | 47/100 | 14 days ago |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| hiromaily/go-crypto-wallet.cursor/rules/typescript.mdc · 126 | Cursor rules | setupbuildtestlint-format+6 | 100/100 | 14 days ago | |
| TechSquidTV/Hermes.cursor/rules/10-hermes-api.mdc · 46 | Cursor rules | testlint-formatstylearch+5 | 100/100 | 14 days ago | |
| dodgecfr/combatfilms-webapp.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 14 days ago | |
| deifos/clipmira-subtitles.cursor/rules/frontend.mdc · 1 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 14 days ago | |
| markstev/mark-starter.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+6 | 99/100 | 14 days ago | |
| Allymahmoud/case-intake-platform.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 14 days ago | |
| langflow-ai/langflow.cursor/rules/docs_development.mdc · 153k | Cursor rules | setupbuildtestlint-format+7 | 97/100 | 14 days ago | |
| TechSquidTV/Hermes.cursor/rules/20-hermes-api-tests.mdc · 46 | Cursor rules | teststyletesting-strategysecurity+3 | 97/100 | 14 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/nerds-odd-e-doughnut-cursor-rules-frontend-api)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.