skills/hono-best-practices/SKILL.md
Hono route handlers, Effect-TS integration, error mapping, middleware, validation, and architectural conventions for the API layer (`api/`). Use when creating or editing any handler, middleware, or route registration. Supersedes the former hono-api-patterns skill.
npx skillsauth add bkinsey808/songshare-effect hono-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Requires: file-read, terminal (linting/testing). No network access needed.
Full reference: /docs/server/hono-best-practices.md
Add-endpoint workflow: Read .agent/workflows/add-api-endpoint.md before writing any handler code.
Companion skills (load on demand):
docs/ai/rules.md for repo-wide constraints..agent/workflows/add-api-endpoint.md.shared/src/validation/ — never in api/src/.handleHttpEndpoint.decodeUnknownSyncOrThrow + dedicated extract function.npm run lint — do not skip.Handlers, not controllers — one handler function per action file; no class-based controllers. Path-param type inference breaks inside classes. → full detail
ReadonlyContext — never Context<{ Bindings: Bindings }>.
→ full detail
handleHttpEndpoint — all Effect handlers register through this wrapper in server.ts.
→ full detail
Path constants only — import from shared/src/paths.ts; never hardcode route strings.
→ full detail
Keep handlers thin — HTTP glue only; business logic in pure service functions with no Hono imports. → full detail
Extract function for validation — schema in shared/src/validation/, decodeUnknownSyncOrThrow in a dedicated extract*.ts; catch and fail with ValidationError.
→ full detail
Throw inside Supabase try — throw on result.error inside the try callback so one catch handles all branches; prevents "all if-else branches same code" lint error.
→ full detail
Always check ownership — fetch user_id from DB and compare before any mutation.
→ full detail
No client as any — use typed if/else chains or a shared helper for dynamic table names.
→ full detail
Descriptive query-param variable names — minimum 2 characters (id-length rule).
→ full detail
Correct error class — ValidationError→400, AuthenticationError→401, AuthorizationError→403, NotFoundError→404, DatabaseError→500.
→ full detail
app.route() for scale — mount feature sub-apps; don't grow server.ts unboundedly.
→ full detail
Typed context variables — define AppVariables and pass Hono<{ Variables: AppVariables }> when middleware sets values handlers need.
→ full detail
Global handlers — register app.onError() and app.notFound() after all route definitions.
→ full detail
Test with app.request() — exercises the full Hono stack; prefer over constructing mock contexts.
→ full detail
Write code changes directly. After edits, output a brief bullet list of which conventions were applied and which validation commands were run.
npm run lint or npx tsc -p api/tsconfig.json --noEmit fails, report verbatim and fix before declaring success.lint-error-resolution.npx tsc -p api/tsconfig.json --noEmit # type-check API only
npm run lint # full lint suite
Input: "Add a DELETE /api/songs/:id endpoint"
Expected: Reads add-api-endpoint workflow, creates api/src/songs/delete/deleteSongHandler.ts using ReadonlyContext, creates extract function in api/src/songs/delete/extractDeleteSongRequest.ts, adds path constant to shared/src/paths.ts, registers in server.ts via handleHttpEndpoint, runs lint and tsc.
Input: "Why does my Supabase insert return an error even though the tryPromise catch isn't firing?"
Expected: Explains that Supabase puts errors in the response object, not as thrown exceptions; must throw result.error inside the try callback so the catch branch sees it.
Input: "Add middleware that attaches a request ID to every request"
Expected: Defines AppVariables type with requestId: string, passes it as Hono<{ Variables: AppVariables }>, creates middleware using c.set('requestId', crypto.randomUUID()), registers with app.use('*', ...).
Context<{ Bindings: Bindings }> — use ReadonlyContext.api/src/ — they belong in shared/src/validation/.client as any for dynamic table names.npx eslint directly — always npm run lint.docs/ai/rules.md.effect-ts-patterns.authentication-system.lint-error-resolution.unit-test-best-practices.tools
Zustand state management patterns for this project — store creation, selectors, Immer middleware, async actions with loading states, devtools, persist, and testing. Use when authoring or editing Zustand stores (use*Store files) or components that subscribe to stores. Do NOT use for React component structure or TypeScript-only utilities.
testing
How to write, update, or split skill files in this repo. Use when creating a new SKILL.md, updating an existing one, or deciding whether to put content in a skill vs. docs/.
development
Complete guide for testing React hooks — renderHook, Documentation by Harness, installStore, fixtures, subscription patterns, lint/compiler traps, and pre-completion checklist. Read docs/testing/unit-test-hook-best-practices.md for the full reference.
development
Vitest unit test authoring for this repo — setup, mocking, API handler testing, and common pitfalls for non-hook code. Use when the user asks to add, update, fix, or review unit tests for utilities, components, API handlers, or scripts. Do NOT use for React hook tests — load unit-test-hook-best-practices instead.