@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.
- package/README.md +3 -2
- package/dist/{chunk-KPDHH2AZ.js → chunk-2VONHHPV.js} +10 -3
- package/dist/chunk-2VONHHPV.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/controller-orchestration-queues/SKILL.md +62 -63
- package/skills/controller-orchestration-queues/references/controller-queue-examples.md +66 -42
- package/skills/error-handling-logging/SKILL.md +64 -68
- package/skills/error-handling-logging/templates/app-error.template.md +10 -1
- package/skills/hono/SKILL.md +73 -66
- package/skills/hono/templates/router.template.md +18 -26
- package/skills/react-core/SKILL.md +45 -112
- package/skills/testing-vitest/SKILL.md +68 -61
- package/skills/testing-vitest/references/backend-d1-testing-pyramid.md +69 -0
- package/skills/typescript-clean/SKILL.md +40 -20
- package/skills/valibot-validation/SKILL.md +63 -20
- package/skills/workers-rpc-precision/SKILL.md +129 -0
- package/skills/workers-rpc-precision/references/python-worker-rpc-examples.md +98 -0
- package/dist/chunk-KPDHH2AZ.js.map +0 -1
|
@@ -1,13 +1,51 @@
|
|
|
1
|
-
# Implementation Reference: Controller Orchestration &
|
|
1
|
+
# Implementation Reference: Controller Orchestration, Queues & D1 Transactions
|
|
2
2
|
|
|
3
|
-
Real-world queue adapters (Cloudflare Queues, BullMQ, AWS SQS, In-Memory) and
|
|
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
|
|
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 (
|
|
22
|
-
throw new AppError({
|
|
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((
|
|
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
|
-
##
|
|
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
|
|
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(
|
|
53
|
-
} catch (
|
|
54
|
-
throw new AppError({
|
|
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((
|
|
61
|
-
name:
|
|
62
|
-
data:
|
|
63
|
-
opts: { jobId:
|
|
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
|
-
##
|
|
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
|
|
84
|
-
this.jobs.push({ ...job, id });
|
|
85
|
-
return { jobId
|
|
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,
|
|
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. **
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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. **
|
|
19
|
-
-
|
|
20
|
-
|
|
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. **
|
|
25
|
-
-
|
|
26
|
-
-
|
|
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. **
|
|
29
|
-
-
|
|
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. **
|
|
32
|
-
-
|
|
33
|
-
-
|
|
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
|
-
|
|
47
|
-
return { success: true, data, message };
|
|
48
|
-
}
|
|
37
|
+
---
|
|
49
38
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
+
---
|
|
56
56
|
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 {
|
|
83
|
-
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
-
|
|
105
|
-
-
|
|
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> {
|
package/skills/hono/SKILL.md
CHANGED
|
@@ -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 &
|
|
6
|
+
# Hono Architecture & Thin Route Standards
|
|
7
7
|
|
|
8
|
-
Standardized guide for building robust, scalable
|
|
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/
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
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
|
-
|
|
47
|
-
├──
|
|
48
|
-
│
|
|
49
|
-
│
|
|
50
|
-
│
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
58
|
+
## 3. Thin Route Pattern Example
|
|
68
59
|
|
|
69
60
|
```ts
|
|
70
|
-
import {
|
|
71
|
-
import
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
85
|
+
## 4. Domain Router Assembly
|
|
90
86
|
|
|
91
87
|
```ts
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
```
|