@3lineas/grooveskills 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,96 +3,103 @@ name: hono
3
3
  description: Use when building or modifying Hono APIs, endpoints, middlewares, or actions. TRIGGER when the project imports 'hono', when modifying routes in src/modules/ or actions in src/actions/, or when the user mentions Hono, API endpoints, or routing. Always validate with Valibot (* as v from 'valibot') and adhere to the 'best-practices' skill.
4
4
  ---
5
5
 
6
- # Hono Architecture & Opinionated Best Practices
6
+ # Hono Architecture & Thin Route Standards
7
7
 
8
- Standardized guide for building robust, scalable, enterprise-grade Hono applications with isolated domain layers, strict Valibot validation, and typed Actions.
8
+ Standardized guide for building robust, scalable Hono applications with isolated domain layers, strict Valibot validation, typed Actions, and thin route wrappers.
9
9
 
10
10
  ---
11
11
 
12
12
  ## 1. Golden Rules (Non-negotiable)
13
13
 
14
14
  1. **Standard Schema Validator with Valibot**:
15
- - Always use **Valibot** (`* as v from "valibot"`) with `@hono/standard-validator` (`sValidator`) or `@hono/valibot-validator`.
16
- - Never use Zod.
17
- 2. **Mandatory Path Aliases (`@/...`)**:
18
- - Import all internal modules using configured path aliases (`@/actions/...`, `@/schemas/...`, `@/errors/...`).
19
- 3. **Zero Barrel Files**:
20
- - Never create `index.ts` files that re-export multiple routes or actions.
21
- - Import directly from each specific action and schema file.
15
+ - Always use **Valibot** (`* as v from "valibot"`) with `@hono/valibot-validator` (`vValidator`) or `@hono/standard-validator` (`sValidator`).
16
+ - Use `validationHook` to intercept schema issues and emit a standardized `400 Bad Request` with `ActionResponse<never>`.
17
+
18
+ 2. **Strict Route Wrapper (`createRouteHandler`)**:
19
+ - Endpoints must be thin controllers. Wrap routes with `createRouteHandler({ outputSchema, status }, handler)`.
20
+ - Handler automatically receives `{ db, logger, env, input, ctx }`, handles try/catch normalization to `AppError`, and returns serialized `ActionResponse<T>`.
21
+
22
+ 3. **Strict Identifier Naming**:
23
+ - The Hono context parameter is **ALWAYS** named `ctx` (NEVER `c`).
24
+ - Catch errors are **ALWAYS** named `error` or `caughtError` (NEVER `e` or `err`).
25
+
22
26
  4. **Independent Route per File by Domain**:
23
- - Every single endpoint resides in its own file (`src/modules/{domain}/{action}.ts`).
24
- - Group them under the domain router `src/modules/{domain}/{domain}.router.ts`.
25
- 5. **Business Logic Exclusively in Actions**:
26
- - All business logic lives inside `src/actions/{domain}/{action}.action.ts`.
27
- - Routers are slim controllers: validate input, call the action, return the response.
28
- 6. **Database Connections strictly in Actions**:
29
- - Never execute SQL, D1, Drizzle, or Prisma queries inside routes or middleware.
30
- - The database instance is injected from Hono context (`ctx.get("db")`) into the action function.
31
- 7. **Single Responsibility Principle (SRP)**:
32
- - Each action performs **exactly one** business action.
33
- 8. **Hard File Limit**:
34
- - Maximum **200 lines per file**.
35
- 9. **Unified Error Handling**:
36
- - Throw typed `AppError` instances for business failures.
37
- - Wrap I/O operations in `try / catch`, log with structured `Logger`, and re-throw or propagate to `globalErrorHandler`.
38
- 10. **Courteous and Professional User Feedback**:
39
- - Return polite, engaging, and solution-oriented messages in English (`ActionResponse<T>`).
27
+ - Every single endpoint resides in its own file (`src/routes/modules/{domain}/{action}.route.ts`).
28
+ - Group them under the domain router `src/routes/modules/{domain}/{domain}.router.ts` and mount in root router.
29
+
30
+ 5. **Business Logic Exclusively in Actions / Pipelines**:
31
+ - All business logic lives in `src/actions/{domain}/{action}.action.ts`.
32
+ - Never execute SQL, D1, Drizzle, or Prisma queries directly inside route handlers.
33
+
34
+ 6. **Hard Size Limit: <200 Lines Max**:
35
+ - Keep route endpoints under 60 lines and routers under 50 lines.
36
+
37
+ 7. **Unified Client Response (`ActionResponse<T>`)**:
38
+ - Every API endpoint response (success or failure) conforms to `ActionResponse<T>` (`success`, `message`, `data`, `error`).
40
39
 
41
40
  ---
42
41
 
43
42
  ## 2. Directory Taxonomy
44
43
 
45
44
  ```text
46
- apps/api/src/
47
- ├── actions/ # Pure business logic layer
48
- │ └── auth/
49
- │ ├── register-user.action.ts # One atomic action (< 100 lines)
50
- │ └── login-user.action.ts
51
- ├── modules/ # Thin HTTP controllers
52
- │ └── auth/
53
- │ ├── register-user.ts # Endpoint definition (< 80 lines)
54
- │ └── auth.router.ts # Domain router mounting endpoints (< 60 lines)
55
- ├── schemas/ # Valibot schemas
56
- │ └── auth/
57
- │ └── register-user.schema.ts # Strict input schema (< 50 lines)
58
- ├── errors/
59
- │ ├── app-error.ts # Base application error with auto-logging
60
- │ └── error-handler.ts # Central Hono error handler
61
- └── routers/
62
- └── api.router.ts # Root router exporting ApiRouter type for RPC
45
+ src/
46
+ ├── routes/
47
+ │ ├── index.ts # Main apiRouter mounting domain routers
48
+ │ └── modules/{domain}/
49
+ │ ├── {action}.route.ts # Atomic endpoint (<60 lines)
50
+ │ └── {domain}.router.ts # Domain assembly router (<50 lines)
51
+ ├── dtos/{domain}/ # Valibot schemas (<50 lines)
52
+ ├── actions/{domain}/ # Atomic business logic (<100 lines)
53
+ └── types/ # Isolated domain contracts (<50 lines)
63
54
  ```
64
55
 
65
56
  ---
66
57
 
67
- ## 3. Valibot Validation Example
58
+ ## 3. Thin Route Pattern Example
68
59
 
69
60
  ```ts
70
- import { sValidator } from "@hono/standard-validator";
71
- import * as v from "valibot";
72
-
73
- export const loginSchema = v.object({
74
- email: v.pipe(
75
- v.string("Please provide your email address."),
76
- v.email("Please provide a valid email address.")
77
- ),
78
- password: v.pipe(
79
- v.string("Password is required."),
80
- v.minLength(8, "Your password must be at least 8 characters long.")
81
- ),
82
- });
83
-
84
- export type LoginInput = v.InferOutput<typeof loginSchema>;
61
+ import { Hono } from "hono";
62
+ import { vValidator } from "@hono/valibot-validator";
63
+ import type { AppContext } from "@/types/context/app-context.type";
64
+ import type { CreateUserInput } from "@/types/users/create-user-input.type";
65
+ import { CreateUserDto } from "@/dtos/users/create-user.dto";
66
+ import { UserResponseDto } from "@/dtos/users/user-response.dto";
67
+ import { registerUserAction } from "@/actions/users/register-user.action";
68
+ import { createRouteHandler } from "@/common/utils/route-handler";
69
+ import { validationHook } from "@/common/utils/validation";
70
+
71
+ export const createUserRoute = new Hono<AppContext>().post(
72
+ "/",
73
+ vValidator("json", CreateUserDto, validationHook),
74
+ createRouteHandler<unknown, typeof UserResponseDto, CreateUserInput>(
75
+ { outputSchema: UserResponseDto, status: 201 },
76
+ async ({ db, input }) => {
77
+ return await registerUserAction(db, input!);
78
+ }
79
+ )
80
+ );
85
81
  ```
86
82
 
87
83
  ---
88
84
 
89
- ## 4. Standard Response Contract
85
+ ## 4. Domain Router Assembly
90
86
 
91
87
  ```ts
92
- export interface ActionResponse<T = unknown> {
93
- success: boolean;
94
- message?: string;
95
- error?: string;
96
- data?: T;
97
- }
88
+ import { Hono } from "hono";
89
+ import type { AppContext } from "@/types/context/app-context.type";
90
+ import { createUserRoute } from "./create-user.route";
91
+ import { listUsersRoute } from "./list-users.route";
92
+
93
+ export const usersRouter = new Hono<AppContext>()
94
+ .route("/", createUserRoute)
95
+ .route("/", listUsersRoute);
98
96
  ```
97
+
98
+ ---
99
+
100
+ ## 5. Templates
101
+
102
+ - `./templates/router.template.md`: Atomic route template with `createRouteHandler`.
103
+ - `./templates/action.template.md`: Single responsibility Action template.
104
+ - `./templates/schema.template.md`: Valibot schema template with UX microcopy.
105
+ - `./templates/error-handler.template.md`: Centralized error handler template.
@@ -1,35 +1,27 @@
1
1
  # Hono Route Template
2
2
 
3
- Slim HTTP controller that validates request body with Valibot and delegates directly to the Action.
3
+ Slim HTTP controller that validates request body with Valibot via `validationHook`, uses `createRouteHandler`, and delegates to the Action.
4
4
 
5
5
  ```ts
6
6
  import { Hono } from "hono";
7
- import { sValidator } from "@hono/standard-validator";
8
- import { registerUserSchema } from "@/schemas/auth/register-user.schema.ts";
9
- import { registerUserAction } from "@/actions/auth/register-user.action.ts";
10
- import type { DatabaseConnection } from "@/actions/auth/register-user.action.ts";
7
+ import { vValidator } from "@hono/valibot-validator";
8
+ import type { AppContext } from "@/types/context/app-context.type";
9
+ import type { RegisterUserInput } from "@/types/auth/register-user-input.type";
10
+ import { RegisterUserDto } from "@/dtos/auth/register-user.dto";
11
+ import { UserResponseDto } from "@/dtos/auth/user-response.dto";
12
+ import { registerUserAction } from "@/actions/auth/register-user.action";
13
+ import { createRouteHandler } from "@/common/utils/route-handler";
14
+ import { validationHook } from "@/common/utils/validation";
11
15
 
12
- export interface CustomVariables {
13
- db: DatabaseConnection;
14
- }
15
-
16
- export const registerUserRoute = new Hono<{ Variables: CustomVariables }>().post(
16
+ export const registerUserRoute = new Hono<AppContext>().post(
17
17
  "/",
18
- sValidator("json", registerUserSchema),
19
- async (ctx) => {
20
- const input = ctx.req.valid("json");
21
- const db = ctx.get("db");
22
-
23
- const createdUser = await registerUserAction(db, input);
24
-
25
- return ctx.json(
26
- {
27
- success: true,
28
- message: "Welcome aboard! Your account has been created successfully.",
29
- data: createdUser,
30
- },
31
- 201
32
- );
33
- }
18
+ vValidator("json", RegisterUserDto, validationHook),
19
+ createRouteHandler<unknown, typeof UserResponseDto, RegisterUserInput>(
20
+ { outputSchema: UserResponseDto, status: 201 },
21
+ async ({ db, input }) => {
22
+ const createdUser = await registerUserAction(db, input!);
23
+ return createdUser;
24
+ }
25
+ )
34
26
  );
35
27
  ```
@@ -11,158 +11,91 @@ Canonical reference for React fundamentals in GrooveSkills projects: function co
11
11
 
12
12
  ## 1. Golden Rules (Non-negotiable)
13
13
 
14
- 1. **One Component per File & < 200 Lines**:
14
+ 1. **Strictly Prohibited: `React.FC` and `React.FunctionComponent`**:
15
+ - In modern React (React 18 / 19), **NEVER** type components with `React.FC` or `React.FunctionComponent`.
16
+ - Declare standard functions and type `props` directly in the parameter list.
17
+ - For components requiring children, type `children: ReactNode` explicitly in the props interface.
18
+
19
+ 2. **One Component per File & < 200 Lines**:
15
20
  - Every exported React component resides in its own dedicated `.tsx` file and never exceeds **200 lines**.
16
21
  - Extract sub-components or custom hooks (`use-*.ts`) into atomic files before reaching the limit.
17
- 2. **Mandatory Path Aliases (`@/...`) & Zero Barrel Files**:
22
+
23
+ 3. **Domain Custom Hooks for Form Encapsulation**:
24
+ - Form components must encapsulate their own state using domain hooks (e.g. `useLoginForm`, `useRegisterForm`) instead of bubbling raw `form` and `onSubmit` props up to higher-level screen containers.
25
+
26
+ 4. **Mandatory Path Aliases (`@/...`) & Zero Barrel Files**:
18
27
  - Import components, hooks, schemas, and utilities directly via path aliases (`@/components/ui/card`, `@/hooks/use-window-width`).
19
28
  - Never create `index.ts` or `index.tsx` barrel files to re-export components or hooks.
20
- 3. **Strict Props Typing (Zero `any`)**:
21
- - Define explicit `interface` or `type` declarations for component props.
22
- - Treat `props` and `state` as strictly immutable.
23
- 4. **Early Returns for UI States (No Nested Ternaries)**:
29
+
30
+ 5. **Early Returns for UI States (No Nested Ternaries)**:
24
31
  - Handle loading, error, and empty states with top-level guard clauses (`if (!data) return <EmptyState />;`) instead of deeply nested JSX conditionals.
25
- 5. **Top-Level Hooks Only (Rules of Hooks)**:
26
- - Call hooks unconditionally at the top level of function components or custom hooks—never inside loops, conditions, or nested functions (except React 19's `use()`, covered in `react-advanced`).
32
+
27
33
  6. **Immutable State Updates & Functional Updaters**:
28
34
  - Never mutate arrays or objects in place (`items.push()`). Always pass a new reference (`[...items, next]`).
29
35
  - Use functional updaters (`setCount((prev) => prev + step)`) whenever next state depends on previous state.
30
- 7. **Compiler-Aware Memoization**:
31
- - Do not wrap every value or callback in `useMemo` or `useCallback` by reflex. When the React Compiler is active, it memoizes automatically.
32
- - Reserve manual `useMemo` and `useCallback` for stabilizing references passed to `useEffect` dependency arrays or for genuinely expensive computations in uncompiled modules.
33
- 8. **Stable Identity Keys in Lists**:
36
+
37
+ 7. **Stable Identity Keys in Lists**:
34
38
  - Always provide a stable, unique `key` derived from domain identity (`item.id`), never the array index (`i`) for dynamic or reorderable lists.
35
- 9. **Controlled Inputs with Single Source of Truth**:
36
- - Derive input `value` from state and handle updates via `onChange`, validating user input with **Valibot** (`* as v from "valibot"`) where applicable.
37
39
 
38
40
  ---
39
41
 
40
- ## 2. Components, JSX, Props, and Early Returns
41
-
42
- Name components in `PascalCase`. Return a single root element or Fragment (`<>...</>`). Use guard clauses to keep the primary render path flat.
42
+ ## 2. Component Typing Standards
43
43
 
44
44
  ```tsx
45
- import { useState } from "react";
45
+ import type { ReactNode } from "react";
46
46
 
47
- interface UserSummaryProps {
47
+ // ✅ Component with props
48
+ export interface UserAvatarProps {
48
49
  name: string;
49
- role?: string;
50
- isActive: boolean;
51
- step?: number;
50
+ avatarUrl?: string;
52
51
  }
53
52
 
54
- export function UserSummary({ name, role = "Member", isActive, step = 1 }: UserSummaryProps) {
55
- const [count, setCount] = useState<number>(0);
56
-
57
- if (!isActive) {
58
- return <p className="text-muted">This account is currently inactive.</p>;
59
- }
60
-
53
+ export function UserAvatar({ name, avatarUrl }: UserAvatarProps) {
61
54
  return (
62
- <section className="user-summary">
63
- <h2>Hello, {name} ({role})</h2>
64
- <button type="button" onClick={() => setCount((prev) => prev + step)}>
65
- Count: {count}
66
- </button>
67
- </section>
55
+ <div className="flex items-center gap-2">
56
+ {avatarUrl ? <img src={avatarUrl} alt={name} /> : null}
57
+ <span>{name}</span>
58
+ </div>
68
59
  );
69
60
  }
70
- ```
71
-
72
- ---
73
-
74
- ## 3. Core Hooks Guide
75
-
76
- - **`useState`**: Holds reactive local state (`const [isOpen, setIsOpen] = useState<boolean>(false);`).
77
- - **`useEffect`**: Synchronizes with external systems (DOM listeners, timers, subscriptions). Always declare accurate dependencies and return a cleanup function.
78
- - **`useRef`**: Holds a mutable value across renders without causing re-renders, or references a DOM node.
79
- - **`useMemo` & `useCallback`**: Deliberate escape hatches to stabilize references for `useEffect` dependency arrays or heavy computations in uncompiled components.
80
- - **`useContext`**: Reads shared state from a `Context` higher in the tree (`<ThemeContext value={theme}>` in React 19).
81
-
82
- ```tsx
83
- import { useEffect, useRef, useState } from "react";
84
-
85
- export function AutoFocusWindowTracker() {
86
- const inputRef = useRef<HTMLInputElement>(null);
87
- const [width, setWidth] = useState<number>(() => window.innerWidth);
88
-
89
- useEffect(() => {
90
- inputRef.current?.focus();
91
- const handleResize = (): void => setWidth(window.innerWidth);
92
- window.addEventListener("resize", handleResize);
93
- return () => window.removeEventListener("resize", handleResize);
94
- }, []);
95
-
96
- return <input ref={inputRef} placeholder={`Viewport: ${width}px`} />;
97
- }
98
- ```
99
-
100
- ---
101
-
102
- ## 4. Composition, Lists, and Controlled Inputs
103
-
104
- Favor composition via `children` over bloated configuration props, use stable domain keys (`todo.id`) for lists, and bind controlled inputs to state:
105
-
106
- ```tsx
107
- import { useState, type ReactNode } from "react";
108
-
109
- export interface TodoItem {
110
- id: string;
111
- text: string;
112
- done: boolean;
113
- }
114
61
 
115
- export function TodoCard({
116
- title,
117
- todos,
118
- children,
119
- }: {
62
+ // ✅ Component with children
63
+ export interface CardProps {
120
64
  title: string;
121
- todos: TodoItem[];
122
- children?: ReactNode;
123
- }) {
124
- const [filter, setFilter] = useState<string>("");
125
-
126
- if (todos.length === 0) {
127
- return <p>Your task list is clear. Add a new item to get started.</p>;
128
- }
129
-
130
- const visible = todos.filter((t) => t.text.toLowerCase().includes(filter.toLowerCase()));
65
+ children: ReactNode;
66
+ }
131
67
 
68
+ export function Card({ title, children }: CardProps) {
132
69
  return (
133
- <article className="card">
134
- <h2>{title}</h2>
135
- <input
136
- value={filter}
137
- onChange={(event) => setFilter(event.target.value)}
138
- placeholder="Filter tasks..."
139
- />
140
- <ul>
141
- {visible.map((todo) => (
142
- <li key={todo.id}>{todo.done ? <s>{todo.text}</s> : todo.text}</li>
143
- ))}
144
- </ul>
70
+ <section className="rounded-lg border p-4">
71
+ <h3 className="font-semibold text-lg">{title}</h3>
145
72
  {children}
146
- </article>
73
+ </section>
147
74
  );
148
75
  }
76
+
77
+ // ✅ Component without props
78
+ export function EmptyState() {
79
+ return <p className="text-muted-foreground text-sm">No items found.</p>;
80
+ }
149
81
  ```
150
82
 
151
83
  ---
152
84
 
153
- ## 5. Anti-Patterns to Avoid
85
+ ## 3. Anti-Patterns to Avoid
154
86
 
155
87
  | Anti-Pattern | Why It Fails | Correct Pattern |
156
88
  | :--- | :--- | :--- |
89
+ | `export const Button: React.FC<Props>` | Verbose, breaks TS generics, legacy implicit children | `export function Button({ label }: ButtonProps)` |
157
90
  | Mutating state in place (`items.push(next)`) | Same array reference; React skips re-render | Create a new reference: `setItems((prev) => [...prev, next])` |
158
- | Calling hooks after `if` or inside loops | Breaks React's deterministic hook call order | Call hooks unconditionally at the top level; branch on results |
91
+ | Calling hooks after `if` or inside loops | Breaks React's deterministic hook call order | Call hooks unconditionally at top level |
159
92
  | Using array index `key={i}` on dynamic lists | Misattributes component state on insert, delete, or sort | Use a stable domain identifier: `key={item.id}` |
160
- | Fetching or deriving synchronous state inside `useEffect` | Triggers an extra render pass and causes UI flicker | Derive values inline during render; use `use()` / Suspense or Actions for async data (`react-advanced`) |
161
93
  | Re-exporting components via `components/index.ts` | Violates Zero Barrel Files and harms tree-shaking | Import directly from `@/components/ui/card` |
162
94
 
163
95
  ---
164
96
 
165
- ## 6. Sibling Skills
97
+ ## 4. Sibling Skills
166
98
 
167
99
  - **`best-practices`**: Universal clean architecture, `< 200` lines limit, `@/` path aliases, strict typing, and courteous user feedback.
168
- - **`react-advanced`**: React 19 platform features, React Compiler 1.0 auto-memoization, concurrent rendering (`useTransition`, `useDeferredValue`, `Suspense`), `use()`, Actions (`useActionState`, `useOptimistic`, `useFormStatus`), context optimization, virtualization, and RSC boundaries.
100
+ - **`react-advanced`**: React 19 platform features, React Compiler auto-memoization, concurrent rendering, and RSC boundaries.
101
+ - **`react-create-page`**: Page coordinator architecture, custom domain hooks, and Valibot forms.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: testing-vitest
3
- description: Clean, deterministic unit and integration testing with Vitest. Enforces Arrange-Act-Assert (AAA), behavioral testing over implementation details, in-memory fakes over brittle mocks, strict assertions, and boundary/edge case test matrices. TRIGGER when writing, maintaining, or fixing automated tests (*.test.ts, *.spec.ts), test utilities, or testing infrastructure with Vitest.
3
+ description: Clean, deterministic unit, integration, and HTTP route testing with Vitest. Enforces Arrange-Act-Assert (AAA), behavioral testing over implementation details, in-memory SQLite/D1 fakes over brittle mocks, four-tier testing pyramid, and strict assertion contracts. TRIGGER when writing, maintaining, or fixing automated tests (*.test.ts, *.spec.ts), test utilities, or testing infrastructure with Vitest.
4
4
  ---
5
5
 
6
- # Vitest Testing Standards
6
+ # Vitest Testing Standards & 4-Tier Backend Pyramid
7
7
 
8
- Guidelines for authoring reliable, fast, and maintainable unit and integration tests with **Vitest**.
8
+ Guidelines for authoring reliable, fast, and maintainable unit, integration, and route tests with **Vitest**.
9
9
 
10
10
  ---
11
11
 
@@ -22,88 +22,95 @@ Guidelines for authoring reliable, fast, and maintainable unit and integration t
22
22
  - `// Assert`: Verify outputs and invariants.
23
23
 
24
24
  3. **In-Memory Fakes Over Brittle Mocks**:
25
- - Prefer in-memory fakes (e.g., `FakeUserRepository`, `MemoryStorage`) over deeply nested `vi.spyOn()` or mock chains.
26
- - Only mock external boundaries (network HTTP calls, system clocks, third-party APIs).
25
+ - Prefer in-memory fakes (e.g., `testDb` backed by `:memory:` SQLite / D1 emulator) over mock chains.
26
+ - Only mock external boundaries (network HTTP calls, system clocks, third-party payment APIs).
27
27
 
28
- 4. **Deterministic & Isolated (Zero Shared Mutable State)**:
29
- - Each test must run independently in any order without relying on state from preceding tests.
30
- - Use `beforeEach` strictly to reset test fixtures.
28
+ 4. **Four-Tier Testing Pyramid**:
29
+ - **Tier 1 (DTOs / Schemas)**: Test constraint validations, sanitization, trims, and invalid payload rejections.
30
+ - **Tier 2 (Actions)**: Test isolated business logic functions with typed context.
31
+ - **Tier 3 (Transactions)**: Test multi-mutation atomic batching and rollback mechanics.
32
+ - **Tier 4 (HTTP Routes)**: Mount thin routes on `testApp`, inject `testDb`, and verify HTTP lifecycle with `testApp.request()`.
31
33
 
32
- 5. **Exhaustive Edge Cases & Failure Modes**:
33
- - Always test boundary values: empty strings, zero, negative numbers, missing optional fields, expired tokens, and rejected promises.
34
- - Never write happy-path-only tests.
34
+ 5. **Mandatory Response Envelope Validation (`ActionResponse<T>`)**:
35
+ - Route tests must assert that responses adhere to `ActionResponse<T>` (`success`, `message`, `data`, `error`).
36
+ - Verify that sensitive fields (e.g. `passwordHash`) are stripped by output whitelisting schemas.
35
37
 
36
- 6. **Clear, Intention-Revealing Test Names**:
37
- - Use descriptive sentences: `it("should calculate tax accurately when order total exceeds threshold")`.
38
- - Avoid vague names like `it("works")` or `it("test 1")`.
38
+ 6. **Hard Size Limit: <200 Lines**:
39
+ - Keep test files focused and under 200 lines (target: 30-100 lines).
39
40
 
40
41
  ---
41
42
 
42
- ## 2. Standard Test Pattern (AAA)
43
-
44
- ```ts
45
- import { describe, it, expect } from "vitest";
46
- import { calculateDiscount } from "./calculate-discount";
47
-
48
- describe("calculateDiscount", () => {
49
- it("should apply 15% discount when user is a premium member", () => {
50
- // Arrange
51
- const originalPrice = 100;
52
- const isPremium = true;
53
-
54
- // Act
55
- const finalPrice = calculateDiscount(originalPrice, isPremium);
43
+ ## 2. Directory Taxonomy
44
+
45
+ ```text
46
+ packages/testing/src/backend/ (or tests/)
47
+ ├── setup.ts # Global testDb initialization & migrations
48
+ ├── mocks/
49
+ │ └── in-memory-d1.ts # In-memory SQLite emulator applying migrations
50
+ └── {domain}/
51
+ ├── {entity}-dto.test.ts # Tier 1: Schema validation tests
52
+ ├── {domain}-actions.test.ts# Tier 2: Pipeline actions unit tests
53
+ └── {action}.route.test.ts # Tier 4: Route integration tests
54
+ ```
56
55
 
57
- // Assert
58
- expect(finalPrice).toBe(85);
59
- });
56
+ ---
60
57
 
61
- it("should return unchanged price when discount percentage is zero", () => {
62
- // Arrange & Act
63
- const finalPrice = calculateDiscount(50, false);
58
+ ## 3. Four-Tier Pattern Examples
64
59
 
65
- // Assert
66
- expect(finalPrice).toBe(50);
60
+ ### Tier 1: DTO Validation Test
61
+ ```ts
62
+ import { describe, it, expect } from "vitest";
63
+ import * as v from "valibot";
64
+ import { CreateUserDto } from "@/dtos/users/create-user.dto";
65
+
66
+ describe("Users DTO Validation", () => {
67
+ it("should accept valid payload and lowercase email", () => {
68
+ const input = { fullName: "Carlos Santana", email: "CARLOS@EXAMPLE.COM" };
69
+ const parsed = v.parse(CreateUserDto, input);
70
+ expect(parsed.email).toBe("carlos@example.com");
67
71
  });
68
72
 
69
- it("should throw an AppError when input price is negative", () => {
70
- // Arrange, Act & Assert
71
- expect(() => calculateDiscount(-10, true)).toThrowError(/Price must be non-negative/);
73
+ it("should reject invalid email", () => {
74
+ expect(() => v.parse(CreateUserDto, { fullName: "Carlos", email: "invalid" })).toThrow();
72
75
  });
73
76
  });
74
77
  ```
75
78
 
76
- ---
77
-
78
- ## 3. Mocking External Boundaries Safely
79
-
79
+ ### Tier 4: Route Integration Test with `testApp.request()`
80
80
  ```ts
81
- import { describe, it, expect, vi, beforeEach } from "vitest";
82
- import { fetchExchangeRate } from "./fetch-exchange-rate";
83
-
84
- describe("fetchExchangeRate", () => {
85
- beforeEach(() => {
86
- vi.restoreAllMocks();
81
+ import { describe, it, expect, vi } from "vitest";
82
+ import { Hono } from "hono";
83
+ import { createUserRoute } from "@/routes/modules/users/create-user.route";
84
+ import { testDb } from "@/backend/setup";
85
+ import type { AppContext } from "@/types/context/app-context.type";
86
+ import type { ActionResponse } from "@/types/common/action-response.type";
87
+
88
+ describe("POST /users Route Integration", () => {
89
+ const testApp = new Hono<AppContext>();
90
+ testApp.use("*", async (ctx, next) => {
91
+ ctx.set("db", testDb);
92
+ ctx.set("logger", { info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any);
93
+ await next();
87
94
  });
95
+ testApp.route("/users", createUserRoute);
88
96
 
89
- it("should parse rate safely when API returns 200", async () => {
90
- const fakeFetch = vi.fn().mockResolvedValue({
91
- ok: true,
92
- json: async () => ({ rate: 1.08 }),
97
+ it("should return 201 with ActionResponse contract", async () => {
98
+ const response = await testApp.request("/users", {
99
+ method: "POST",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify({ fullName: "Valeria Gomez", email: "valeria@example.com" }),
93
102
  });
94
- vi.stubGlobal("fetch", fakeFetch);
95
-
96
- const rate = await fetchExchangeRate("EUR", "USD");
97
103
 
98
- expect(rate).toBe(1.08);
99
- expect(fakeFetch).toHaveBeenCalledWith(expect.stringContaining("EUR/USD"));
104
+ expect(response.status).toBe(201);
105
+ const json = (await response.json()) as ActionResponse<{ id: string; passwordHash?: string }>;
106
+ expect(json.success).toBe(true);
107
+ expect(json.data?.passwordHash).toBeUndefined();
100
108
  });
101
109
  });
102
110
  ```
103
111
 
104
112
  ---
105
113
 
106
- ## 4. Sibling Skills
114
+ ## 4. Deep References
107
115
 
108
- - **`best-practices`**: Universal clean code rules.
109
- - **`error-handling-logging`**: Testing expected `AppError` throws and error payloads.
116
+ - `./references/backend-d1-testing-pyramid.md`: Comprehensive guide for In-Memory SQLite/D1 setup and test matrices.