@3lineas/grooveskills 1.7.2 → 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.
@@ -1,13 +1,51 @@
1
- # Implementation Reference: Controller Orchestration & Queue Adapters
1
+ # Implementation Reference: Controller Orchestration, Queues & D1 Transactions
2
2
 
3
- Real-world queue adapters (Cloudflare Queues, BullMQ, AWS SQS, In-Memory) and Slim Worker controllers.
3
+ Real-world queue adapters (Cloudflare Queues, BullMQ, AWS SQS, In-Memory) and atomic Cloudflare D1 transaction runner.
4
4
 
5
5
  ---
6
6
 
7
- ## 1. Cloudflare Queues Adapter
7
+ ## 1. Cloudflare D1 Atomic Transaction Runner (`withD1Transaction`)
8
+
9
+ Cloudflare D1 runs SQLite at the edge, where standard multi-statement transactions are executed via `db.batch()`. The `withD1Transaction` pattern allows actions to stage statements and execute them atomically:
10
+
11
+ ```ts
12
+ import type { DrizzleD1Database } from "drizzle-orm/d1";
13
+ import type { BatchItem } from "drizzle-orm/batch";
14
+
15
+ export interface D1TransactionContext<TSchema extends Record<string, unknown>> {
16
+ readonly db: DrizzleD1Database<TSchema>;
17
+ stage: (statement: BatchItem<"sqlite">) => void;
18
+ }
19
+
20
+ export async function withD1Transaction<TSchema extends Record<string, unknown>, TResult>(
21
+ database: DrizzleD1Database<TSchema>,
22
+ callback: (transaction: D1TransactionContext<TSchema>) => Promise<TResult>
23
+ ): Promise<TResult> {
24
+ const stagedStatements: BatchItem<"sqlite">[] = [];
25
+
26
+ const transactionContext: D1TransactionContext<TSchema> = {
27
+ db: database,
28
+ stage: (statement) => {
29
+ stagedStatements.push(statement);
30
+ },
31
+ };
32
+
33
+ const result = await callback(transactionContext);
34
+
35
+ if (stagedStatements.length > 0) {
36
+ // All staged mutations are executed in a single atomic batch
37
+ await database.batch(stagedStatements as [BatchItem<"sqlite">, ...BatchItem<"sqlite">[]]);
38
+ }
39
+
40
+ return result;
41
+ }
42
+ ```
43
+
44
+ ---
45
+
46
+ ## 2. Cloudflare Queues Adapter
8
47
 
9
48
  ```ts
10
- // src/infrastructure/adapters/queue/cloudflare-queues.adapter.ts
11
49
  import type { QueuePort, QueueJob } from "@/domains/queue/queue.port";
12
50
  import { AppError } from "@/errors/app-error";
13
51
 
@@ -18,13 +56,17 @@ export class CloudflareQueueAdapter implements QueuePort {
18
56
  try {
19
57
  await this.queue.send(job, { delaySeconds: job.delaySeconds });
20
58
  return { jobId: job.id ?? crypto.randomUUID() };
21
- } catch (err) {
22
- throw new AppError({ message: "Error al encolar mensaje en Cloudflare Queues.", statusCode: 502, cause: err });
59
+ } catch (caughtError) {
60
+ throw new AppError({
61
+ message: "Error al encolar mensaje en Cloudflare Queues.",
62
+ statusCode: 502,
63
+ cause: caughtError,
64
+ });
23
65
  }
24
66
  }
25
67
 
26
68
  async dispatchBatch<T>(jobs: QueueJob<T>[]): Promise<{ count: number }> {
27
- await this.queue.sendBatch(jobs.map((j) => ({ body: j, delaySeconds: j.delaySeconds })));
69
+ await this.queue.sendBatch(jobs.map((job) => ({ body: job, delaySeconds: job.delaySeconds })));
28
70
  return { count: jobs.length };
29
71
  }
30
72
  }
@@ -32,10 +74,9 @@ export class CloudflareQueueAdapter implements QueuePort {
32
74
 
33
75
  ---
34
76
 
35
- ## 2. BullMQ (Redis) Adapter
77
+ ## 3. BullMQ (Redis) Adapter
36
78
 
37
79
  ```ts
38
- // src/infrastructure/adapters/queue/bullmq-queues.adapter.ts
39
80
  import { Queue as BullQueue } from "bullmq";
40
81
  import type { QueuePort, QueueJob } from "@/domains/queue/queue.port";
41
82
  import { AppError } from "@/errors/app-error";
@@ -45,22 +86,26 @@ export class BullMQAdapter implements QueuePort {
45
86
 
46
87
  async dispatch<T>(job: QueueJob<T>): Promise<{ jobId: string }> {
47
88
  try {
48
- const bJob = await this.bullQueue.add(job.name, job.data, {
89
+ const queuedJob = await this.bullQueue.add(job.name, job.data, {
49
90
  jobId: job.id,
50
91
  delay: job.delaySeconds ? job.delaySeconds * 1000 : undefined,
51
92
  });
52
- return { jobId: String(bJob.id) };
53
- } catch (err) {
54
- throw new AppError({ message: "Error al enviar trabajo a BullMQ.", statusCode: 502, cause: err });
93
+ return { jobId: String(queuedJob.id) };
94
+ } catch (caughtError) {
95
+ throw new AppError({
96
+ message: "Error al enviar trabajo a BullMQ.",
97
+ statusCode: 502,
98
+ cause: caughtError,
99
+ });
55
100
  }
56
101
  }
57
102
 
58
103
  async dispatchBatch<T>(jobs: QueueJob<T>[]): Promise<{ count: number }> {
59
104
  await this.bullQueue.addBulk(
60
- jobs.map((j) => ({
61
- name: j.name,
62
- data: j.data,
63
- opts: { jobId: j.id, delay: j.delaySeconds ? j.delaySeconds * 1000 : undefined },
105
+ jobs.map((job) => ({
106
+ name: job.name,
107
+ data: job.data,
108
+ opts: { jobId: job.id, delay: job.delaySeconds ? job.delaySeconds * 1000 : undefined },
64
109
  }))
65
110
  );
66
111
  return { count: jobs.length };
@@ -70,19 +115,18 @@ export class BullMQAdapter implements QueuePort {
70
115
 
71
116
  ---
72
117
 
73
- ## 3. In-Memory Queue Adapter (Testing Fake)
118
+ ## 4. In-Memory Queue Adapter (Testing Fake)
74
119
 
75
120
  ```ts
76
- // src/infrastructure/adapters/queue/memory-queues.adapter.ts
77
121
  import type { QueuePort, QueueJob } from "@/domains/queue/queue.port";
78
122
 
79
123
  export class MemoryQueueAdapter implements QueuePort {
80
124
  public jobs: QueueJob[] = [];
81
125
 
82
126
  async dispatch<T>(job: QueueJob<T>): Promise<{ jobId: string }> {
83
- const id = job.id ?? `job_${this.jobs.length + 1}`;
84
- this.jobs.push({ ...job, id });
85
- return { jobId: id };
127
+ const jobId = job.id ?? `job_${this.jobs.length + 1}`;
128
+ this.jobs.push({ ...job, id: jobId });
129
+ return { jobId };
86
130
  }
87
131
 
88
132
  async dispatchBatch<T>(jobs: QueueJob<T>[]): Promise<{ count: number }> {
@@ -93,23 +137,3 @@ export class MemoryQueueAdapter implements QueuePort {
93
137
  }
94
138
  }
95
139
  ```
96
-
97
- ---
98
-
99
- ## 4. Slim Queue Worker Controller Pattern
100
-
101
- ```ts
102
- // src/infrastructure/workers/order-events.worker.ts
103
- import { sendOrderEmailAction } from "@/actions/email/send-order-email.action";
104
- import { syncCrmAction } from "@/actions/crm/sync-crm.action";
105
- import { logger } from "@/infrastructure/logger/app-logger";
106
-
107
- export async function handleOrderQueueMessage(messageBody: { name: string; data: { orderId: string; customerEmail: string } }) {
108
- logger.info("Procesando evento de orden", { jobName: messageBody.name, orderId: messageBody.data.orderId });
109
-
110
- if (messageBody.name === "orders.post-checkout") {
111
- await sendOrderEmailAction(messageBody.data.customerEmail, messageBody.data.orderId);
112
- await syncCrmAction(messageBody.data.orderId);
113
- }
114
- }
115
- ```
@@ -1,99 +1,95 @@
1
1
  ---
2
2
  name: error-handling-logging
3
- description: Deterministic error handling with custom AppError, Result/ActionResponse contracts, and structured logging. TRIGGER when designing error boundaries, writing try/catch blocks, handling I/O or network failures, creating domain exceptions, or configuring application loggers.
3
+ description: Deterministic error handling with custom AppError, domain exception hierarchies, silent mode, Result/ActionResponse contracts, and structured logging. TRIGGER when designing error boundaries, writing try/catch blocks, handling I/O or network failures, creating domain exceptions, or configuring application loggers.
4
4
  ---
5
5
 
6
6
  # Error Handling & Structured Logging Standards
7
7
 
8
- Enterprise-grade patterns for deterministic error handling, structured context logging, and safe API error responses.
8
+ Enterprise-grade patterns for deterministic error handling, typed domain exceptions, silent log mode for expected business flows, and safe API error responses.
9
9
 
10
10
  ---
11
11
 
12
12
  ## 1. Golden Rules (Non-negotiable)
13
13
 
14
- 1. **Typed Domain Errors with `AppError`**:
15
- - Never throw raw strings or generic untyped `Error("something failed")`.
16
- - Throw structured `AppError` instances with HTTP status code, error code identifier, user-safe message, and optional contextual metadata.
14
+ 1. **Hierarchy Based on `AppError`**:
15
+ - Every domain error or business exception **MUST** extend `AppError`.
16
+ - Never throw raw strings or generic untyped `Error("failed")` in domain actions or controllers.
17
17
 
18
- 2. **Never Swallow Errors Silently**:
19
- - Every `catch (err)` block must either:
20
- - Recover with an explicit fallback value.
21
- - Log with `Logger.error(...)` and re-throw / return an `ActionResponse` failure.
22
- - Empty catch blocks (`catch {}`) without explicit intent are forbidden.
18
+ 2. **Atomic Domain Exceptions (1 Exception per File)**:
19
+ - Each custom exception resides in its own atomic file: `@/errors/{domain}/{name}.error.ts` (e.g. `slug-conflict.error.ts`, `user-not-found.error.ts`).
20
+ - Throw custom errors directly without boilerplate try/catch re-wrapping inside business logic.
23
21
 
24
- 3. **Separate User-Safe Messages from Internal Diagnostics**:
25
- - The user receives polite, actionable feedback (e.g. *"Unable to complete transaction. Please try again."*).
26
- - Internal logs receive complete diagnostic metadata (stack trace, payload params, correlation ID, elapsed time).
22
+ 3. **Auto-Logging with `silent: true` for Expected Flows**:
23
+ - `AppError` emits a warning via structured logger upon instantiation.
24
+ - For expected user conflicts (e.g. 409 duplicate slug, 404 not found, 400 validation error), set `silent: true` in the constructor to avoid polluting server error alerts.
27
25
 
28
- 4. **Result / ActionResponse for Non-fatal Domain Failures**:
29
- - Prefer returning `{ success: false, error: "..." }` or `Result<T, E>` across service boundaries when the failure is an expected domain outcome (e.g. validation failure, duplicate email).
26
+ 4. **Universal Error Normalization in Catch Blocks**:
27
+ - Every top-level catch block or global error handler must normalize unknown or unexpected runtime errors into `AppError` subclasses (e.g. `InternalServerError`).
28
+ - All HTTP responses are formatted via standardized response serialization (`ActionResponse<never>`).
30
29
 
31
- 5. **Structured JSON-Ready Logging**:
32
- - Avoid `console.log("here 1")` in production code.
33
- - Use a structured `Logger` utility emitting timestamp, level (`DEBUG`, `INFO`, `WARN`, `ERROR`), message, and contextual metadata object.
30
+ 5. **Separate User-Safe Messages from Internal Diagnostics**:
31
+ - The user receives courteous, empathetic, and actionable feedback in the configured locale (governed by `ux-writing`).
32
+ - Internal logs receive complete diagnostic metadata (stack trace, payload params, correlation ID).
34
33
 
35
- ---
36
-
37
- ## 2. Standard Contracts
38
-
39
- ### A. The ActionResponse Contract
40
-
41
- ```ts
42
- export type ActionResponse<TData = unknown, TError = string> =
43
- | { success: true; data: TData; message?: string }
44
- | { success: false; error: TError; message?: string; statusCode?: number };
34
+ 6. **Hard Size Limit of <200 Lines**:
35
+ - Keep atomic exception classes under 40 lines and error handlers concise.
45
36
 
46
- export function successResponse<T>(data: T, message?: string): ActionResponse<T> {
47
- return { success: true, data, message };
48
- }
37
+ ---
49
38
 
50
- export function errorResponse(error: string, statusCode = 400): ActionResponse<never> {
51
- return { success: false, error, statusCode };
52
- }
39
+ ## 2. Directory Layout
40
+
41
+ ```text
42
+ src/
43
+ ├── errors/
44
+ │ ├── app-error.ts # Base AppError with auto-logging & silent flag
45
+ │ ├── common/
46
+ │ │ ├── internal-server.error.ts # 500 error for unhandled exceptions
47
+ │ │ └── validation.error.ts # 400 error for schema validation
48
+ │ └── {domain}/ # Domain-specific custom exceptions
49
+ │ ├── slug-conflict.error.ts # e.g., extends AppError (409, silent: true)
50
+ │ └── entity-not-found.error.ts # e.g., extends AppError (404, silent: true)
51
+ └── types/errors/
52
+ └── app-error-params.type.ts # AppError constructor options
53
53
  ```
54
54
 
55
- ### B. The `AppError` Base Class
55
+ ---
56
56
 
57
- See `templates/app-error.template.md` for full implementation.
57
+ ## 3. Standard Contracts & Domain Exceptions
58
58
 
59
+ ### A. Domain Custom Exception Example (`src/errors/stores/slug-conflict.error.ts`)
59
60
  ```ts
60
- export class AppError extends Error {
61
- readonly statusCode: number;
62
- readonly code: string;
63
- readonly details?: unknown;
64
-
65
- constructor(opts: { message: string; statusCode?: number; code?: string; details?: unknown }) {
66
- super(opts.message);
67
- this.name = "AppError";
68
- this.statusCode = opts.statusCode ?? 500;
69
- this.code = opts.code ?? "INTERNAL_SERVER_ERROR";
70
- this.details = opts.details;
61
+ import { AppError } from "@/errors/app-error";
62
+
63
+ export class SlugConflictError extends AppError {
64
+ constructor(
65
+ slug: string,
66
+ message = `El enlace '${slug}' ya está registrado. Te sugerimos agregar una palabra o número adicional.`
67
+ ) {
68
+ super({
69
+ message,
70
+ statusCode: 409,
71
+ code: "SLUG_ALREADY_IN_USE",
72
+ details: { slug },
73
+ silent: true, // Expected business conflict: suppress error log alert
74
+ });
75
+ this.name = "SlugConflictError";
71
76
  }
72
77
  }
73
78
  ```
74
79
 
75
- ---
76
-
77
- ## 3. Safe Try/Catch Pattern
78
-
80
+ ### B. Universal Catch Normalization Pattern
79
81
  ```ts
80
- import { Logger } from "@/utils/logger";
81
82
  import { AppError } from "@/errors/app-error";
82
- import { type ActionResponse, errorResponse, successResponse } from "@/types/response.types";
83
-
84
- export async function executeSafeTask<T>(task: () => Promise<T>): Promise<ActionResponse<T>> {
85
- try {
86
- const result = await task();
87
- return successResponse(result);
88
- } catch (error: unknown) {
89
- if (error instanceof AppError) {
90
- Logger.warn("Expected domain error caught", { code: error.code, message: error.message });
91
- return errorResponse(error.message, error.statusCode);
92
- }
93
-
94
- Logger.error("Unhandled runtime exception in task execution", { error });
95
- return errorResponse("An unexpected error occurred. Please try again later.", 500);
83
+ import { InternalServerError } from "@/errors/common/internal-server.error";
84
+
85
+ export function normalizeError(caughtError: unknown): AppError {
86
+ if (caughtError instanceof AppError) {
87
+ return caughtError;
96
88
  }
89
+ return new InternalServerError({
90
+ message: "Ocurrió un error inesperado al procesar la solicitud.",
91
+ cause: caughtError,
92
+ });
97
93
  }
98
94
  ```
99
95
 
@@ -101,5 +97,5 @@ export async function executeSafeTask<T>(task: () => Promise<T>): Promise<Action
101
97
 
102
98
  ## 4. Templates
103
99
 
104
- - `templates/app-error.template.md`: Complete extensible `AppError` implementation with serialization.
105
- - `templates/logger.template.md`: Lightweight structured logger with log levels and JSON output formatting.
100
+ - `./templates/app-error.template.md`: Complete extensible `AppError` base class with `silent` mode.
101
+ - `./templates/logger.template.md`: Lightweight structured logger with contextual metadata.
@@ -1,14 +1,18 @@
1
- # Template: AppError Class
1
+ # Template: AppError Base Class
2
2
 
3
3
  Copy or adapt this template when setting up error handling in your project: `src/errors/app-error.ts`.
4
4
 
5
5
  ```ts
6
+ import { Logger } from "@/utils/logger";
7
+
6
8
  export interface AppErrorOptions {
7
9
  message: string;
8
10
  statusCode?: number;
9
11
  code?: string;
10
12
  details?: unknown;
11
13
  cause?: unknown;
14
+ /** When true, suppresses automatic warning logging for expected business conflicts */
15
+ silent?: boolean;
12
16
  }
13
17
 
14
18
  export class AppError extends Error {
@@ -28,6 +32,11 @@ export class AppError extends Error {
28
32
  if (Error.captureStackTrace) {
29
33
  Error.captureStackTrace(this, AppError);
30
34
  }
35
+
36
+ if (!options.silent) {
37
+ const logger = new Logger(`API:Error:${this.name}`);
38
+ logger.warn(`${this.code}: ${this.message}`, { details: this.details });
39
+ }
31
40
  }
32
41
 
33
42
  toJSON(): Record<string, unknown> {
@@ -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
  ```