From bc179a51e5e695073808b07f0ff6deca5ee66cef Mon Sep 17 00:00:00 2001 From: Marco Comi Date: Fri, 30 Jan 2026 11:53:39 +0100 Subject: [PATCH 1/4] Align interactive handler types and Tasks client Make interactive handlers async and add a TasksClient to centralize API calls so server-only logic remains server-side and the UI can await operations consistently. This changes handler signatures (onTaskComplete now returns Promise); update any call sites to the async signature. --- .../src/app/api/tasks/[taskId]/route.ts | 23 ++++++ apps/to-do-webapp/src/app/api/tasks/route.ts | 17 +++++ apps/to-do-webapp/src/app/page.tsx | 63 ++-------------- .../src/components/TasksClient.tsx | 71 +++++++++++++++++++ apps/to-do-webapp/src/components/ToDoItem.tsx | 25 ++++--- apps/to-do-webapp/src/components/ToDoList.tsx | 11 ++- .../src/components/ToDoTextArea.tsx | 23 +++--- 7 files changed, 147 insertions(+), 86 deletions(-) create mode 100644 apps/to-do-webapp/src/app/api/tasks/[taskId]/route.ts create mode 100644 apps/to-do-webapp/src/app/api/tasks/route.ts create mode 100644 apps/to-do-webapp/src/components/TasksClient.tsx diff --git a/apps/to-do-webapp/src/app/api/tasks/[taskId]/route.ts b/apps/to-do-webapp/src/app/api/tasks/[taskId]/route.ts new file mode 100644 index 00000000..c9e3a926 --- /dev/null +++ b/apps/to-do-webapp/src/app/api/tasks/[taskId]/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; + +import { completeTask } from "@/lib/api"; + +export async function DELETE( + request: Request, + context: { params: Promise> | Record }, +) { + try { + // In Next.js App Router dynamic API handlers `context.params` may be a Promise. + // Await it before accessing properties to avoid the sync dynamic APIs error. + const resolvedParams = await context.params; + const taskId: string = resolvedParams?.taskId as string; + + const { status } = await completeTask(taskId); + return new NextResponse(null, { status }); + } catch { + return NextResponse.json( + { message: "Error deleting task" }, + { status: 500 }, + ); + } +} diff --git a/apps/to-do-webapp/src/app/api/tasks/route.ts b/apps/to-do-webapp/src/app/api/tasks/route.ts new file mode 100644 index 00000000..209b74cf --- /dev/null +++ b/apps/to-do-webapp/src/app/api/tasks/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server"; + +import { insertTask } from "@/lib/api"; + +export async function POST(req: Request) { + try { + const body = await req.json(); + const { title } = body; + const { status, value } = await insertTask(title); + return NextResponse.json(value, { status }); + } catch { + return NextResponse.json( + { message: "Error creating task" }, + { status: 500 }, + ); + } +} diff --git a/apps/to-do-webapp/src/app/page.tsx b/apps/to-do-webapp/src/app/page.tsx index b09a6eff..52b771de 100644 --- a/apps/to-do-webapp/src/app/page.tsx +++ b/apps/to-do-webapp/src/app/page.tsx @@ -1,62 +1,11 @@ -"use client"; - -import { Alert, Container, Divider, Typography } from "@mui/material"; -import { useEffect, useState } from "react"; - -import ToDoList from "@/components/ToDoList"; -import ToDoTextArea from "@/components/ToDoTextArea"; +import TasksClient from "@/components/TasksClient"; import { getTaskList } from "@/lib/api"; -import { TaskItem } from "@/lib/client/TaskItem"; -import { TaskItemList } from "@/lib/client/TaskItemList"; -export const dynamic = "force-dynamic"; - -export default function Home() { - const [tasks, setTasks] = useState([]); - const [error, setError] = useState(null); - useEffect(() => { - const fetchTasks = async () => { - try { - const { status, value: taskList } = await getTaskList(); - if (status === 200) { - setError(null); - setTasks(taskList); - } else { - setError("Failed to fetch tasks. Please try again later."); - setTasks([]); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - setError("Failed to fetch tasks. Please try again later."); - } - }; - fetchTasks(); - }, []); - - // Update the list of tasks with the new task - const addTask = (task: TaskItem) => { - setTasks((prevTasks) => [...prevTasks, task]); - }; - - // Remove the completed task from the list - const completeTask = (taskId: string) => { - setTasks((tasks) => tasks.filter(({ id }) => id !== taskId)); - }; - - return ( - - - My Tasks - - +export const dynamic = "force-dynamic"; - +export default async function Home() { + const { status, value: taskList } = await getTaskList(); + const initialTasks = status === 200 ? taskList : []; - {error ? ( - {error} - ) : ( - - )} - - ); + return ; } diff --git a/apps/to-do-webapp/src/components/TasksClient.tsx b/apps/to-do-webapp/src/components/TasksClient.tsx new file mode 100644 index 00000000..6af0ed3c --- /dev/null +++ b/apps/to-do-webapp/src/components/TasksClient.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { Alert, Container, Divider, Typography } from "@mui/material"; +import { useState } from "react"; + +import type { TaskItemList } from "@/lib/client/TaskItemList"; + +import ToDoList from "@/components/ToDoList"; +import ToDoTextArea from "@/components/ToDoTextArea"; + +export default function TasksClient({ + initialTasks, +}: { + initialTasks: TaskItemList; +}) { + const [tasks, setTasks] = useState(initialTasks); + const [error, setError] = useState(null); + + const addTask = async (title: string): Promise => { + setError(null); + try { + const res = await fetch("/api/tasks", { + body: JSON.stringify({ title }), + headers: { "Content-Type": "application/json" }, + method: "POST", + }); + if (res.ok) { + const created = await res.json(); + setTasks((p) => [...p, created]); + } else { + const body = await res.json().catch(() => null); + setError(body?.message ?? "Failed creating task"); + } + } catch (err) { + setError("Failed creating task"); + throw err instanceof Error ? err : new Error(String(err)); + } + }; + + const completeTask = async (id: string): Promise => { + setError(null); + try { + const res = await fetch(`/api/tasks/${id}`, { method: "DELETE" }); + if (res.ok) { + setTasks((p) => p.filter((t) => t.id !== id)); + } else { + setError("Failed deleting task"); + } + } catch (err) { + setError("Failed deleting task"); + throw err instanceof Error ? err : new Error(String(err)); + } + }; + + return ( + + + My Tasks + + + + + + {error ? ( + {error} + ) : ( + + )} + + ); +} diff --git a/apps/to-do-webapp/src/components/ToDoItem.tsx b/apps/to-do-webapp/src/components/ToDoItem.tsx index 8881c8de..bccbee0c 100644 --- a/apps/to-do-webapp/src/components/ToDoItem.tsx +++ b/apps/to-do-webapp/src/components/ToDoItem.tsx @@ -1,13 +1,11 @@ import { Checkbox, ListItem, ListItemText } from "@mui/material"; import React from "react"; -import { completeTask } from "@/lib/api"; import { TaskId } from "@/lib/client/TaskId"; interface TodoItemProps { id: TaskId; - // Function to execute when a task is completed - onComplete: (id: TaskId) => void; + onComplete: (id: TaskId) => Promise; state: "COMPLETED" | "DELETED" | "INCOMPLETE"; title: string; } @@ -18,22 +16,23 @@ const ToDoItem: React.FC = ({ state, title, }) => { - const handleTaskComplete = async (taskId: TaskId) => { + const [isLoading, setIsLoading] = React.useState(false); + + const handleChange = async () => { + setIsLoading(true); try { - const { status } = await completeTask(taskId); - if (status === 204) { - onComplete(id); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - /* error when deleting the task */ + await onComplete(id); + } finally { + setIsLoading(false); } }; + return ( - + handleTaskComplete(id)} + disabled={isLoading} + onChange={handleChange} /> diff --git a/apps/to-do-webapp/src/components/ToDoList.tsx b/apps/to-do-webapp/src/components/ToDoList.tsx index b5cbdd50..4f08acef 100644 --- a/apps/to-do-webapp/src/components/ToDoList.tsx +++ b/apps/to-do-webapp/src/components/ToDoList.tsx @@ -7,7 +7,8 @@ import { TaskItemList } from "@/lib/client/TaskItemList"; interface TaskListProps { // Function to execute when a task is completed - onTaskComplete: (id: TaskId) => void; + // Function to execute when a task is completed (async) + onTaskComplete: (id: TaskId) => Promise; tasks: TaskItemList; } @@ -19,7 +20,13 @@ const ToDoList: React.FC = ({ {tasks .filter(({ state }) => state === "INCOMPLETE") .map((item) => ( - + ))} ); diff --git a/apps/to-do-webapp/src/components/ToDoTextArea.tsx b/apps/to-do-webapp/src/components/ToDoTextArea.tsx index 9f1c40a1..b14c6ad9 100644 --- a/apps/to-do-webapp/src/components/ToDoTextArea.tsx +++ b/apps/to-do-webapp/src/components/ToDoTextArea.tsx @@ -1,13 +1,12 @@ import { Button, TextField } from "@mui/material"; import React, { useState } from "react"; -import { insertTask } from "@/lib/api"; -import { TaskItem } from "@/lib/client/TaskItem"; +// TaskItem type removed; this component is presentational and only deals with titles interface Props { label: string; - // Function to update the list of tasks with the new task - onAddTask: (task: TaskItem) => void; + // Parent must return a promise so the component can await and show loading + onAddTask: (taskTitle: string) => Promise; } const ToDoTextArea = ({ label, onAddTask }: Props) => { @@ -18,14 +17,10 @@ const ToDoTextArea = ({ label, onAddTask }: Props) => { const handleAddTask = async (text: string) => { setIsLoading(true); try { - const { status, value } = await insertTask(text); - if (status === 201) { - setTaskText(""); - onAddTask(value); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - /* error when creating a task */ + await onAddTask(text); + setTaskText(""); + } catch { + // let parent show error } finally { setIsLoading(false); } @@ -52,10 +47,10 @@ const ToDoTextArea = ({ label, onAddTask }: Props) => { ); From d2b21aa82c6396cced64adafca39d959f4637058 Mon Sep 17 00:00:00 2001 From: Marco Comi Date: Fri, 30 Jan 2026 12:05:34 +0100 Subject: [PATCH 2/4] Validate to-do webapp env on server Validate required environment variables on the server to fail fast with clear errors and to prevent leaking runtime secrets into client bundles. Run pnpm install to add zod if needed and ensure .env.local is created from .env.local.example; verify the validation module is only imported on the server. --- apps/to-do-webapp/README.md | 20 +++++++++++++++++ apps/to-do-webapp/src/lib/client.ts | 35 ++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/apps/to-do-webapp/README.md b/apps/to-do-webapp/README.md index fa5dac1b..4316b025 100644 --- a/apps/to-do-webapp/README.md +++ b/apps/to-do-webapp/README.md @@ -18,6 +18,26 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Environment + +This app expects several server-only environment variables to be set before running. To avoid accidental leaks, do not commit `.env.local` with secrets. Create a local file from your environment or use your secrets manager. + +Required variables (server-only): + +- `API_BASE_URL` — base URL of the backend API +- `API_BASE_PATH` — base path used by the generated API client +- `API_KEY` — API key for server-to-server calls (keep secret) + +When running locally you can copy a provided example (if present) or create `.env.local` yourself: + +```bash +# create a local env and edit values +cp .env.local.example .env.local 2>/dev/null || true +# then edit .env.local to set API_BASE_URL/API_BASE_PATH/API_KEY +``` + +The app validates required env vars on the server at startup and will fail fast with a clear error if any are missing. + This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ## Learn More diff --git a/apps/to-do-webapp/src/lib/client.ts b/apps/to-do-webapp/src/lib/client.ts index e8614326..b551fa99 100644 --- a/apps/to-do-webapp/src/lib/client.ts +++ b/apps/to-do-webapp/src/lib/client.ts @@ -1,18 +1,41 @@ +import { z } from "zod"; + import { createClient, WithDefaultsT } from "@/lib/client/client"; +// Server-only environment validation: ensure required env vars exist and +// fail-fast with a clear error message. This module must remain server-side +// to avoid leaking secrets into client bundles. +const EnvSchema = z.object({ + API_BASE_PATH: z.string().nonempty(), + API_BASE_URL: z.string().nonempty(), + API_KEY: z.string().nonempty(), + APPINSIGHTS_SAMPLING_PERCENTAGE: z.preprocess((v) => { + if (v == null || v === "") return undefined; + return Number(v); + }, z.number().optional()), + APPLICATIONINSIGHTS_CONNECTION_STRING: z.string().optional(), + OTEL_SERVICE_NAME: z.string().optional(), +}); + +// Only perform validation on the server. If this module is accidentally +// imported on the client, avoid throwing and instead rely on server routes +// to validate. `typeof window === 'undefined'` indicates server runtime. +let env: undefined | z.infer; +if (typeof window === "undefined") { + env = EnvSchema.parse(process.env); +} + const withApiKey: WithDefaultsT<"ApiKeyAuth"> = (wrappedOperation) => (params) => wrappedOperation({ ...params, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ApiKeyAuth: process.env.API_KEY!, + // At runtime on the server we validated env above; assert here for TS. + ApiKeyAuth: (env?.API_KEY ?? process.env.API_KEY) as string, }); export const client = createClient({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - basePath: process.env.API_BASE_PATH!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - baseUrl: process.env.API_BASE_URL!, + basePath: (env?.API_BASE_PATH ?? process.env.API_BASE_PATH) as string, + baseUrl: (env?.API_BASE_URL ?? process.env.API_BASE_URL) as string, fetchApi: fetch as unknown as typeof fetch, withDefaults: withApiKey, }); From 90f72a5b354ad0ec5c28239ccdabb36fea545589 Mon Sep 17 00:00:00 2001 From: Marco Comi Date: Fri, 30 Jan 2026 12:13:17 +0100 Subject: [PATCH 3/4] Validate to-do webapp env with Zod on server Validate required environment variables on the server using Zod to avoid leaking secrets and to fail fast when configuration is missing. Run pnpm install to add zod and ensure .env.local contains the required vars; keep this module server-only to avoid client-side imports. --- apps/to-do-webapp/package.json | 3 +- apps/to-do-webapp/src/lib/client.ts | 50 +++++++++++++++++------------ pnpm-lock.yaml | 41 +++++++++++------------ 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/apps/to-do-webapp/package.json b/apps/to-do-webapp/package.json index c245cbda..23581d3c 100644 --- a/apps/to-do-webapp/package.json +++ b/apps/to-do-webapp/package.json @@ -21,7 +21,8 @@ "io-ts": "catalog:", "next": "catalog:next", "react": "catalog:react", - "react-dom": "catalog:react" + "react-dom": "catalog:react", + "zod": "^4.3.5" }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", diff --git a/apps/to-do-webapp/src/lib/client.ts b/apps/to-do-webapp/src/lib/client.ts index b551fa99..65e082bf 100644 --- a/apps/to-do-webapp/src/lib/client.ts +++ b/apps/to-do-webapp/src/lib/client.ts @@ -17,25 +17,35 @@ const EnvSchema = z.object({ OTEL_SERVICE_NAME: z.string().optional(), }); -// Only perform validation on the server. If this module is accidentally -// imported on the client, avoid throwing and instead rely on server routes -// to validate. `typeof window === 'undefined'` indicates server runtime. -let env: undefined | z.infer; -if (typeof window === "undefined") { - env = EnvSchema.parse(process.env); -} +// Create the API client on the server only. This will parse and validate +// environment variables and throw immediately if any required value is +// missing or invalid — preventing the app from starting with a bad config. +function createServerClient() { + const env = EnvSchema.parse(process.env); // throws on validation error -const withApiKey: WithDefaultsT<"ApiKeyAuth"> = - (wrappedOperation) => (params) => - wrappedOperation({ - ...params, - // At runtime on the server we validated env above; assert here for TS. - ApiKeyAuth: (env?.API_KEY ?? process.env.API_KEY) as string, - }); + const withApiKey: WithDefaultsT<"ApiKeyAuth"> = + (wrappedOperation) => (params) => + wrappedOperation({ + ...params, + ApiKeyAuth: env.API_KEY, + }); -export const client = createClient({ - basePath: (env?.API_BASE_PATH ?? process.env.API_BASE_PATH) as string, - baseUrl: (env?.API_BASE_URL ?? process.env.API_BASE_URL) as string, - fetchApi: fetch as unknown as typeof fetch, - withDefaults: withApiKey, -}); + return createClient<"ApiKeyAuth">({ + basePath: env.API_BASE_PATH, + baseUrl: env.API_BASE_URL, + fetchApi: fetch as unknown as typeof fetch, + withDefaults: withApiKey, + }); +} + +// Export a server-only client. If this module is imported on the client, +// accessing `client` will throw a clear error (and it will not contain +// server secrets). This keeps env values out of client bundles. +export const client = + typeof window === "undefined" + ? createServerClient() + : ((): never => { + throw new Error( + "Attempted to initialize server-only API client on the client. Ensure this module is only imported from server code.", + ); + })(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4492a113..6628035d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,7 +227,7 @@ importers: version: 9.39.2 eslint-config-next: specifier: catalog:next - version: 16.1.6(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + version: 16.1.6(eslint@9.39.2)(typescript@5.9.3) typescript: specifier: 'catalog:' version: 5.9.3 @@ -447,6 +447,9 @@ importers: react-dom: specifier: catalog:react version: 19.2.4(react@19.2.4) + zod: + specifier: ^4.3.5 + version: 4.3.5 devDependencies: '@eslint/eslintrc': specifier: ^3.3.3 @@ -477,7 +480,7 @@ importers: version: 9.39.2 eslint-config-next: specifier: catalog:next - version: 16.1.6(eslint@9.39.2)(typescript@5.9.3) + version: 16.1.6(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint@9.39.2)(typescript@5.9.3) prettier: specifier: 'catalog:' version: 3.8.1 @@ -7928,10 +7931,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint@9.39.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.53.0 '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2)(typescript@5.9.3) '@typescript-eslint/utils': 8.53.0(eslint@9.39.2)(typescript@5.9.3) @@ -7985,7 +7988,7 @@ snapshots: dependencies: '@typescript-eslint/scope-manager': 8.53.0 '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 eslint: 9.39.2 @@ -8288,14 +8291,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.7))': - dependencies: - '@vitest/spy': 4.0.18 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@22.19.7) - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.0.10))': dependencies: '@vitest/spy': 4.0.18 @@ -9112,13 +9107,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.1.6(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3): + eslint-config-next@16.1.6(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint@9.39.2)(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.1.6 eslint: 9.39.2 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2) eslint-plugin-react: 7.37.5(eslint@9.39.2) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2) @@ -9175,7 +9170,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2) transitivePeerDependencies: - supports-color @@ -9194,11 +9189,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.8.3) eslint: 9.39.2 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2) @@ -9215,7 +9210,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9226,7 +9221,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -9238,7 +9233,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -11380,7 +11375,7 @@ snapshots: typescript-eslint@8.53.0(eslint@9.39.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.8.3))(eslint@9.39.2)(typescript@5.9.3) '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) '@typescript-eslint/utils': 8.53.0(eslint@9.39.2)(typescript@5.9.3) @@ -11499,7 +11494,7 @@ snapshots: vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.7)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 From 2a9bc988f76774223720e8c138dbf9f206d81ac0 Mon Sep 17 00:00:00 2001 From: Marco Comi Date: Fri, 30 Jan 2026 12:19:31 +0100 Subject: [PATCH 4/4] Stop rethrowing errors from TasksClient Errors are now logged and surfaced via component state instead of being rethrown. This prevents uncaught exceptions and lets the UI present consistent error states. Follow-up: audit other clients for the same pattern and add tests to assert error handling is reflected in state. --- apps/to-do-webapp/src/components/TasksClient.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/to-do-webapp/src/components/TasksClient.tsx b/apps/to-do-webapp/src/components/TasksClient.tsx index 6af0ed3c..7c7496b4 100644 --- a/apps/to-do-webapp/src/components/TasksClient.tsx +++ b/apps/to-do-webapp/src/components/TasksClient.tsx @@ -32,8 +32,8 @@ export default function TasksClient({ setError(body?.message ?? "Failed creating task"); } } catch (err) { - setError("Failed creating task"); - throw err instanceof Error ? err : new Error(String(err)); + console.error("TasksClient.addTask error", err); + setError(err instanceof Error ? err.message : "Failed creating task"); } }; @@ -47,8 +47,8 @@ export default function TasksClient({ setError("Failed deleting task"); } } catch (err) { - setError("Failed deleting task"); - throw err instanceof Error ? err : new Error(String(err)); + console.error("TasksClient.completeTask error", err); + setError(err instanceof Error ? err.message : "Failed deleting task"); } };