Implementation Changes: Utility Extraction
Precise code modification instructions for Part 2 of Component Preparation. Each section corresponds to a group in the specification.
Source references point to files in arda-frontend-app.
Group E — Error Handling
Section titled “Group E — Error Handling”New: src/lib/errors.ts
Section titled “New: src/lib/errors.ts”/** * Extract a human-readable message from an unknown error value. */export function extractErrorMessage(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; return String(error);}New: src/lib/errors.test.ts
Section titled “New: src/lib/errors.test.ts”Test cases:
Error('boom')→'boom''plain string'→'plain string'42→'42'null→'null'undefined→'undefined'{ code: 'ERR' }→'[object Object]'
Consumer updates
Section titled “Consumer updates”Replace inline patterns in API route catch blocks. Example transformation in a typical route handler:
Before:
details: error instanceof Error ? error.message : 'Unknown error',After:
import { extractErrorMessage } from '@/lib/errors';// ...details: extractErrorMessage(error),Apply to all src/app/api/arda/**/route.ts files that contain the pattern.
Also apply to src/lib/authErrorHandler.ts and src/lib/utils.ts
(isAuthenticationError function’s initial narrowing).
Group G — Environment Constants
Section titled “Group G — Environment Constants”New: src/lib/env-spa.ts
Section titled “New: src/lib/env-spa.ts”/** * Client-side environment constants. * Use these instead of reading process.env.NEXT_PUBLIC_* directly. */export const IS_PRODUCTION = process.env.NEXT_PUBLIC_DEPLOY_ENV === 'PRODUCTION';
export const IS_STAGE = process.env.NEXT_PUBLIC_DEPLOY_ENV === 'STAGE';
export const IS_MOCK_MODE = process.env.NEXT_PUBLIC_MOCK_MODE === 'true';New: src/lib/env-spa.test.ts
Section titled “New: src/lib/env-spa.test.ts”Test cases using jest.replaceProperty or module re-import with modified
process.env:
NEXT_PUBLIC_DEPLOY_ENV=PRODUCTION→IS_PRODUCTIONistrue,IS_STAGEisfalseNEXT_PUBLIC_DEPLOY_ENV=STAGE→IS_STAGEistrue,IS_PRODUCTIONisfalseNEXT_PUBLIC_MOCK_MODE=true→IS_MOCK_MODEistrue- No env vars set → all are
false
Modify: src/lib/utils.ts
Section titled “Modify: src/lib/utils.ts”Before (lines 44-60):
export function debugLog(...args: unknown[]) { if (process.env.NEXT_PUBLIC_DEPLOY_ENV === 'STAGE') { console.log(...args); }}// same pattern for debugError, debugWarnAfter:
import { IS_STAGE } from '@/lib/env-spa';
export function debugLog(...args: unknown[]) { if (IS_STAGE) { console.log(...args); }}// same pattern for debugError, debugWarnConsumer updates
Section titled “Consumer updates”Replace scattered process.env.NEXT_PUBLIC_* comparisons with constant
imports. Key files:
src/lib/authErrorHandler.ts— replaceprocess.env.NEXT_PUBLIC_DEPLOY_ENV !== 'PRODUCTION'with!IS_PRODUCTIONsrc/app/items/page.tsx— replace mock-mode checks withIS_MOCK_MODEsrc/store/thunks/authThunks.ts— replace environment checks
Group F — Formatters
Section titled “Group F — Formatters”New: src/lib/formatters.ts
Section titled “New: src/lib/formatters.ts”import type * as domain from '@/types/domain';import type * as items from '@/types/items';
export function formatDate(date: string | undefined): string { if (!date) return '-'; return new Date(date).toLocaleDateString();}
export function formatDateTime(date: string | undefined): string { if (!date) return '-'; return new Date(date).toLocaleString();}
export function formatCurrency(value: domain.Money | undefined): string { if (!value || value.value == null) return '-'; return `$${value.value.toFixed(2)} ${value.currency}`;}
export function formatQuantity(quantity: items.Quantity | undefined): string { if (!quantity) return '-'; return `${quantity.amount} ${quantity.unit}`;}New: src/lib/formatters.test.ts
Section titled “New: src/lib/formatters.test.ts”Test each function with valid input and undefined input. For
formatCurrency, also test { value: null, currency: 'USD' }.
Modify: src/components/table/columnPresets.tsx
Section titled “Modify: src/components/table/columnPresets.tsx”Remove the inline formatDate, formatDateTime, formatCurrency,
formatQuantity function definitions. Add:
import { formatDate, formatDateTime, formatCurrency, formatQuantity } from '@/lib/formatters';All valueFormatter references remain unchanged — they already call these
function names.
Modify: src/lib/ardaClient.ts
Section titled “Modify: src/lib/ardaClient.ts”Remove the local formatQuantity function (lines 56-59). Add:
import { formatQuantity } from '@/lib/formatters';Group D — Safe Serialization
Section titled “Group D — Safe Serialization”New: src/lib/storage.ts
Section titled “New: src/lib/storage.ts”/** * Parse JSON without throwing. Returns fallback on any parse error. */export function safeJsonParse<T>(text: string, fallback: T): T { try { return JSON.parse(text) as T; } catch { return fallback; }}
/** * Read a JSON value from localStorage. Returns fallback when the key * is missing or the stored value is not valid JSON. */export function getStorageItem<T>(key: string, fallback: T): T { try { const raw = localStorage.getItem(key); if (raw === null) return fallback; return JSON.parse(raw) as T; } catch { return fallback; }}
/** * Write a JSON value to localStorage. Silently ignores quota errors. */export function setStorageItem(key: string, value: unknown): void { try { localStorage.setItem(key, JSON.stringify(value)); } catch { // Quota exceeded or security restriction — silently ignore }}New: src/lib/storage.test.ts
Section titled “New: src/lib/storage.test.ts”Test cases:
safeJsonParse('{"a":1}', {})→{ a: 1 }safeJsonParse('not json', {})→{}safeJsonParse('', null)→nullgetStorageItemwith key present → parsed valuegetStorageItemwith key missing → fallbackgetStorageItemwith malformed value → fallbacksetStorageItemstores serialized valuesetStorageItemwith quota error → no throw (mocklocalStorage.setItemto throw)
Consumer updates
Section titled “Consumer updates”Replace inline patterns in:
src/app/items/page.tsx— grid state persistencesrc/app/items/ItemTableAGGrid.tsx— grid state save/restoresrc/contexts/SidebarVisibilityContext.tsx— sidebar state persistence
Example transformation:
Before:
const existingGridState = localStorage.getItem(gridStateKey);if (!existingGridState) return;try { const gridState = JSON.parse(existingGridState); // ...} catch { /* ignore */ }After:
import { getStorageItem } from '@/lib/storage';// ...const gridState = getStorageItem(gridStateKey, null);if (!gridState) return;// ...Group A — API Route Infrastructure
Section titled “Group A — API Route Infrastructure”New: src/lib/api-route-utils.ts
Section titled “New: src/lib/api-route-utils.ts”import { NextResponse } from 'next/server';
/** * Generate a random UUID v4 string for use as X-Request-ID. */export function generateRequestId(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); } );}
/** * Read and parse an upstream response. Returns parsed JSON when the * content-type is application/json, otherwise returns { raw: text }. */export async function parseUpstreamResponse( response: Response): Promise<{ data: unknown; contentType: string }> { const text = await response.text(); const contentType = response.headers.get('content-type') || ''; const data = contentType.includes('application/json') ? JSON.parse(text) : { raw: text }; return { data, contentType };}
/** * Build a NextResponse from an upstream fetch result. */export function forwardAsNextResponse( upstream: Response, data: unknown): NextResponse { return NextResponse.json( { ok: upstream.ok, status: upstream.status, data }, { status: upstream.ok ? 200 : upstream.status } );}New: src/lib/api-route-utils.test.ts
Section titled “New: src/lib/api-route-utils.test.ts”Test cases:
generateRequestIdmatches UUID v4 regex (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)- Two calls produce different values
parseUpstreamResponsewith JSON content-type → parsed objectparseUpstreamResponsewithtext/plain→{ raw: '...' }parseUpstreamResponsewith empty body →{ raw: '' }forwardAsNextResponsewith 200 upstream → status 200,ok: trueforwardAsNextResponsewith 404 upstream → status 404,ok: falseforwardAsNextResponsewith 500 upstream → status 500,ok: false
Consumer updates — Route handler template
Section titled “Consumer updates — Route handler template”Each of the 43 route handlers under src/app/api/arda/ follows the same
transformation. Using src/app/api/arda/tenant/query/route.ts as the
canonical example:
Before:
const generateGuid = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); } );};
const headers: Record<string, string> = { 'Content-Type': 'application/json', Authorization: `Bearer ${env.ARDA_API_KEY}`, 'X-Request-ID': generateGuid(), // ...};
const upstream = await fetch(url, { method: 'POST', headers, body, cache: 'no-store' });const text = await upstream.text();const contentType = upstream.headers.get('content-type') || '';const data = contentType.includes('application/json') ? JSON.parse(text) : { raw: text };
return NextResponse.json( { ok: upstream.ok, status: upstream.status, data }, { status: upstream.ok ? 200 : upstream.status });After:
import { generateRequestId, parseUpstreamResponse, forwardAsNextResponse,} from '@/lib/api-route-utils';// ...
const headers: Record<string, string> = { 'Content-Type': 'application/json', Authorization: `Bearer ${env.ARDA_API_KEY}`, 'X-Request-ID': generateRequestId(), // ...};
const upstream = await fetch(url, { method: 'POST', headers, body, cache: 'no-store' });const { data } = await parseUpstreamResponse(upstream);
return forwardAsNextResponse(upstream, data);Apply this transformation to all 43 route files. The implementing engineer should verify that routes with non-standard response handling (e.g., binary responses, streaming) are handled correctly or excluded from the migration.
Group B — Lookup Response Parsing
Section titled “Group B — Lookup Response Parsing”Modify: src/lib/ardaClient.ts
Section titled “Modify: src/lib/ardaClient.ts”Add the shared helper (exported for testing):
/** * Generic lookup against an ARDA API endpoint that returns string values. * Handles three response shapes: * 1. Plain string array: data is string[] * 2. Named field: data[fieldName] is string[] * 3. Results array: data.results[].name */export async function lookupField( endpoint: string, fieldName: string, params: URLSearchParams): Promise<string[]> { const response = await fetch( `/api/arda/${endpoint}?${params.toString()}`, { method: 'GET', headers: await getAuthHeaders() } );
const json = await response.json(); if (!response.ok) { throw new Error(json?.error || `Lookup failed: ${response.status}`); }
const data = json?.data; if (Array.isArray(data)) { return data.filter((x: unknown) => typeof x === 'string'); } if (data && Array.isArray(data[fieldName])) { return data[fieldName].filter((x: unknown) => typeof x === 'string'); } if (data && typeof data === 'object' && Array.isArray(data.results)) { return data.results .map((r: unknown) => typeof r === 'string' ? r : (r as { name?: string })?.name ) .filter(Boolean) as string[]; } return [];}Replace each lookup function. Example for lookupSuppliers:
Before (lines 400-440):
export async function lookupSuppliers( name: string, effectiveasof?: string, recordedasof?: string): Promise<string[]> { const params = new URLSearchParams(); params.set('name', name); if (effectiveasof) params.set('effectiveasof', effectiveasof); if (recordedasof) params.set('recordedasof', recordedasof); // ... 30 lines of fetch + parse + fallback ...}After:
export async function lookupSuppliers( name: string, effectiveasof?: string, recordedasof?: string): Promise<string[]> { const params = new URLSearchParams(); params.set('name', name); if (effectiveasof) params.set('effectiveasof', effectiveasof); if (recordedasof) params.set('recordedasof', recordedasof); return lookupField('items/lookup-suppliers', 'suppliers', params);}Apply the same pattern to all 9 functions:
| Function | Endpoint | Field Name |
|---|---|---|
lookupSuppliers | items/lookup-suppliers | suppliers |
lookupUnits | items/lookup-units | units |
lookupTypes | items/lookup-types | types |
lookupSubtypes | items/lookup-subtypes | subtypes |
lookupUseCases | items/lookup-usecases | usecases |
lookupFacilities | items/lookup-facilities | facilities |
lookupDepartments | items/lookup-departments | departments |
lookupLocations | items/lookup-locations | locations |
lookupSublocations | items/lookup-sublocations | sublocations |
Note: Verify the exact endpoint paths and field names by reading the current implementation of each function before applying the table above.
Group C — JWT / Token Consolidation
Section titled “Group C — JWT / Token Consolidation”Modify: src/lib/jwt.ts
Section titled “Modify: src/lib/jwt.ts”Add a named format validation export (if not already present):
/** * Check whether a string looks like a valid 3-part JWT. */export function isValidJWTFormat(token: string): boolean { return token.split('.').length === 3;}Ensure decodeJWTPayload does not import NextRequest (it currently
doesn’t — the function is already client-safe).
Consumer updates
Section titled “Consumer updates”src/store/thunks/authThunks.ts — replace all inline
JSON.parse(atob(token.split('.')[1])) with:
import { decodeJWTPayload } from '@/lib/jwt';// ...const payload = decodeJWTPayload(token);if (!payload) { /* handle error */ }const expiresAt = payload.exp * 1000;Apply the same replacement in:
src/lib/tokenRefresh.ts(lines 85, 139)src/store/components/AuthInit.tsx(lines 43, 92)src/contexts/AuthContext.tsx(line 485)
src/lib/ardaClient.ts — replace inline format validation (line 86):
Before:
if (accessToken.split('.').length !== 3) {After:
import { isValidJWTFormat } from '@/lib/jwt';// ...if (!isValidJWTFormat(accessToken)) {Apply the same replacement for the idToken check (line 93).
Important: Each replacement must preserve the surrounding error handling logic. The
decodeJWTPayloadfunction returnsnullon failure, which must be checked where the inline version previously could not fail (it would throw, caught by surrounding try/catch).
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved