@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.
@@ -1,24 +1,34 @@
1
1
  ---
2
2
  name: controller-orchestration-queues
3
- description: Strict orchestration standards for Controllers, atomic Actions, database Transactions, and background Queue Adapters (Cloudflare Queues, BullMQ, AWS SQS, In-Memory). Mandates that Actions do only ONE atomic thing, while Controllers sequence multiple actions, manage transactions, or dispatch async jobs. Enforces Queue Workers as slim controllers. TRIGGER when orchestrating multi-step workflows, handling DB transactions across actions, dispatching background tasks, or designing queue consumers.
3
+ description: Strict orchestration standards for Controllers, atomic Actions, Pipeline pattern, database Transactions (including Cloudflare D1 batching), and background Queue Adapters. Mandates that Actions do only ONE atomic thing, while Controllers sequence multiple actions, manage transactions, or dispatch async jobs. Enforces Queue Workers as slim controllers. TRIGGER when orchestrating multi-step workflows, handling DB transactions across actions, dispatching background tasks, or designing queue consumers.
4
4
  ---
5
5
 
6
- # Controller Action Orchestration, Transactions & Queue Adapters
6
+ # Controller Action Orchestration, Pipelines, Transactions & Queues
7
7
 
8
- Architectural standard establishing strict separation of concerns between **Atomic Actions** and **Orchestration Controllers / Queue Workers**.
8
+ Architectural standard establishing strict separation of concerns between **Atomic Actions**, **Declarative Pipelines**, **Transactions (SQL / D1)**, and **Orchestration Controllers / Queue Workers**.
9
9
 
10
10
  ---
11
11
 
12
12
  ## 1. The Core Architectural Law
13
13
 
14
14
  1. **Actions Do Exactly ONE Atomic Thing**:
15
- - Pure single responsibility. Example: `createOrderAction`, `chargeCardAction`, `sendOrderEmailAction`.
16
- - Actions NEVER orchestrate other sibling actions or dispatch unrelated background jobs directly.
17
- 2. **Controllers are Orchestrators**:
18
- - HTTP route handlers and RPC controllers validate inputs, sequence actions, wrap transactional DB boundaries (`withTransaction`), and dispatch background jobs via `QueuePort`.
19
- 3. **Queue Workers are Slim Controllers**:
15
+ - Single responsibility. Example: `sanitizeSlugAction`, `ensureAvailableAction`, `persistOrderAction`.
16
+ - **An Action NEVER calls another Action directly.** No nested Action cascades.
17
+ - Signature: `(context: TContext) => Promise<TContext> | TContext`.
18
+
19
+ 2. **Controllers & Pipelines are Orchestrators**:
20
+ - HTTP route handlers and RPC controllers validate inputs, sequence actions through Pipelines (`Pipeline.send(context).through([...actions])`), wrap transactions, and dispatch background jobs via `QueuePort`.
21
+
22
+ 3. **Atomic Multi-Mutation Pipelines (`withTransaction` / `withD1Transaction`)**:
23
+ - In standard relational databases, wrap actions inside a database transaction handle (`tx`).
24
+ - In serverless SQLite / Cloudflare D1, wrap pipeline executions with `withD1Transaction`: actions run immediate reads via `tx.db.select()` and stage mutations via `tx.stage(stmt)` to guarantee all-or-nothing rollback using `db.batch()`.
25
+
26
+ 4. **Queue Workers are Slim Controllers**:
20
27
  - A queue worker message handler behaves like an HTTP controller: parses payload and delegates to atomic actions.
21
28
 
29
+ 5. **Hard Size Limit of <200 Lines**:
30
+ - Controllers, actions, and pipeline steps must remain strictly below 200 lines (target: 20-60 lines).
31
+
22
32
  ---
23
33
 
24
34
  ## 2. Directory Layout
@@ -26,84 +36,73 @@ Architectural standard establishing strict separation of concerns between **Atom
26
36
  ```text
27
37
  src/
28
38
  ├── actions/
29
- │ ├── orders/
30
- │ │ ├── create-order.action.ts # Atomic insert order (<70 lines)
31
- │ │ └── update-order-status.action.ts # Atomic update status (<60 lines)
32
- │ └── billing/
33
- │ └── charge-card.action.ts # Atomic payment charge (<70 lines)
39
+ │ └── orders/
40
+ │ ├── sanitize-order.action.ts # Atomic data transform (<50 lines)
41
+ │ ├── ensure-stock.action.ts # Atomic invariant check (<50 lines)
42
+ │ └── persist-order.action.ts # Atomic DB persistence (<60 lines)
34
43
  ├── controllers/
35
44
  │ └── orders/
36
- │ └── checkout.controller.ts # Orchestrates actions + Queue (<90 lines)
45
+ │ └── checkout.controller.ts # Pipeline & Queue Orchestration (<80 lines)
37
46
  ├── domains/
38
47
  │ └── queue/
39
48
  │ ├── queue.port.ts # Domain interface for dispatching (<40 lines)
40
49
  │ └── queue-message.types.ts # Strongly typed queue messages (<50 lines)
41
50
  └── infrastructure/
42
- ├── adapters/
43
- │ └── queue/
44
- │ ├── cloudflare-queues.adapter.ts # Cloudflare Queues adapter (<80 lines)
45
- │ ├── bullmq-queues.adapter.ts # Redis / BullMQ adapter (<80 lines)
46
- │ ├── sqs-queues.adapter.ts # AWS SQS adapter (<80 lines)
47
- │ └── memory-queues.adapter.ts # In-memory testing queue (<60 lines)
48
- └── workers/
49
- └── order-events.worker.ts # Slim worker invoking actions (<80 lines)
51
+ └── adapters/
52
+ └── queue/ # Swappable Queue adapters (Cloudflare, BullMQ, Memory)
50
53
  ```
51
54
 
52
55
  ---
53
56
 
54
- ## 3. Domain Queue Contract (`src/domains/queue/queue.port.ts`)
57
+ ## 3. Pipeline Orchestration Pattern
55
58
 
56
59
  ```ts
57
- export interface QueueJob<TData = unknown> {
58
- id?: string;
59
- name: string;
60
- data: TData;
61
- delaySeconds?: number;
62
- }
63
-
64
- export interface QueuePort {
65
- dispatch<T>(job: QueueJob<T>): Promise<{ jobId: string }>;
66
- dispatchBatch<T>(jobs: QueueJob<T>[]): Promise<{ count: number }>;
60
+ import type { Context } from "hono";
61
+ import { Pipeline } from "@/common/utils/pipeline";
62
+ import { sanitizeOrderAction } from "@/actions/orders/sanitize-order.action";
63
+ import { ensureStockAction } from "@/actions/orders/ensure-stock.action";
64
+ import { persistOrderAction } from "@/actions/orders/persist-order.action";
65
+ import type { CheckoutContext } from "@/types/orders/checkout-context.type";
66
+
67
+ export async function checkoutController(ctx: Context) {
68
+ const database = ctx.get("db");
69
+ const validatedPayload = ctx.req.valid("json");
70
+
71
+ const finalContext = await Pipeline.send<CheckoutContext>({
72
+ database,
73
+ input: validatedPayload,
74
+ })
75
+ .through([
76
+ sanitizeOrderAction,
77
+ ensureStockAction,
78
+ persistOrderAction,
79
+ ]);
80
+
81
+ return ctx.json({ success: true, data: finalContext.createdOrder }, 201);
67
82
  }
68
83
  ```
69
84
 
70
85
  ---
71
86
 
72
- ## 4. Orchestration Controller Pattern
87
+ ## 4. Cloudflare D1 Staged Batching (`withD1Transaction`)
88
+
89
+ For Cloudflare D1, wrap mutations in a staged batch to guarantee ACID rollback:
73
90
 
74
91
  ```ts
75
- // src/controllers/orders/checkout.controller.ts
76
- import type { Context } from "hono";
77
- import type { QueuePort } from "@/domains/queue/queue.port";
78
- import { createOrderAction } from "@/actions/orders/create-order.action";
79
- import { chargeCardAction } from "@/actions/billing/charge-card.action";
80
- import { db, withTransaction } from "@/infrastructure/db/connection";
81
-
82
- export async function checkoutController(c: Context, queue: QueuePort) {
83
- const payload = c.get("validatedJson");
84
- const actor = c.get("actor");
85
-
86
- // Step 1: Charge card (external side-effect, kept outside DB transaction)
87
- const charge = await chargeCardAction(payload.paymentMethodId, payload.amount);
88
-
89
- // Step 2: Atomic DB Transaction for local entity creation
90
- const order = await withTransaction(db, async (tx) => {
91
- return createOrderAction(tx, { actorId: actor.id, amount: payload.amount, chargeId: charge.id });
92
- });
93
-
94
- // Step 3: Dispatch async post-processing via Queue
95
- await queue.dispatch({
96
- name: "orders.post-checkout",
97
- data: { orderId: order.id, customerEmail: actor.email },
98
- });
99
-
100
- return c.json({ orderId: order.id, status: "confirmed" }, 201);
92
+ export async function stageOrderMutationAction(context: CheckoutContext): Promise<CheckoutContext> {
93
+ const insertStatement = context.database
94
+ .insert(ordersTable)
95
+ .values(context.newOrderRecord);
96
+
97
+ // Stage statement for atomic db.batch() execution
98
+ context.transaction?.stage(insertStatement);
99
+ return context;
101
100
  }
102
101
  ```
103
102
 
104
103
  ---
105
104
 
106
- ## 5. Detailed Adapters & Workers Reference
105
+ ## 5. Detailed Adapters & References
107
106
 
108
- For real implementations of Cloudflare Queues, BullMQ, AWS SQS, and In-Memory Queue adapters, consult:
109
- - `references/controller-queue-examples.md`
107
+ For real implementations of Cloudflare Queues, BullMQ, AWS SQS, In-Memory Queue adapters, and the full D1 Transaction runner, consult:
108
+ - `./references/controller-queue-examples.md`
@@ -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> {