Skip to content

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.

/**
* 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);
}

Test cases:

  • Error('boom')'boom'
  • 'plain string''plain string'
  • 42'42'
  • null'null'
  • undefined'undefined'
  • { code: 'ERR' }'[object Object]'

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).


/**
* 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';

Test cases using jest.replaceProperty or module re-import with modified process.env:

  • NEXT_PUBLIC_DEPLOY_ENV=PRODUCTIONIS_PRODUCTION is true, IS_STAGE is false
  • NEXT_PUBLIC_DEPLOY_ENV=STAGEIS_STAGE is true, IS_PRODUCTION is false
  • NEXT_PUBLIC_MOCK_MODE=trueIS_MOCK_MODE is true
  • No env vars set → all are false

Before (lines 44-60):

export function debugLog(...args: unknown[]) {
if (process.env.NEXT_PUBLIC_DEPLOY_ENV === 'STAGE') {
console.log(...args);
}
}
// same pattern for debugError, debugWarn

After:

import { IS_STAGE } from '@/lib/env-spa';
export function debugLog(...args: unknown[]) {
if (IS_STAGE) {
console.log(...args);
}
}
// same pattern for debugError, debugWarn

Replace scattered process.env.NEXT_PUBLIC_* comparisons with constant imports. Key files:

  • src/lib/authErrorHandler.ts — replace process.env.NEXT_PUBLIC_DEPLOY_ENV !== 'PRODUCTION' with !IS_PRODUCTION
  • src/app/items/page.tsx — replace mock-mode checks with IS_MOCK_MODE
  • src/store/thunks/authThunks.ts — replace environment checks

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}`;
}

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.

Remove the local formatQuantity function (lines 56-59). Add:

import { formatQuantity } from '@/lib/formatters';

/**
* 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
}
}

Test cases:

  • safeJsonParse('{"a":1}', {}){ a: 1 }
  • safeJsonParse('not json', {}){}
  • safeJsonParse('', null)null
  • getStorageItem with key present → parsed value
  • getStorageItem with key missing → fallback
  • getStorageItem with malformed value → fallback
  • setStorageItem stores serialized value
  • setStorageItem with quota error → no throw (mock localStorage.setItem to throw)

Replace inline patterns in:

  • src/app/items/page.tsx — grid state persistence
  • src/app/items/ItemTableAGGrid.tsx — grid state save/restore
  • src/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;
// ...

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 }
);
}

Test cases:

  • generateRequestId matches 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
  • parseUpstreamResponse with JSON content-type → parsed object
  • parseUpstreamResponse with text/plain{ raw: '...' }
  • parseUpstreamResponse with empty body → { raw: '' }
  • forwardAsNextResponse with 200 upstream → status 200, ok: true
  • forwardAsNextResponse with 404 upstream → status 404, ok: false
  • forwardAsNextResponse with 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.


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:

FunctionEndpointField Name
lookupSuppliersitems/lookup-supplierssuppliers
lookupUnitsitems/lookup-unitsunits
lookupTypesitems/lookup-typestypes
lookupSubtypesitems/lookup-subtypessubtypes
lookupUseCasesitems/lookup-usecasesusecases
lookupFacilitiesitems/lookup-facilitiesfacilities
lookupDepartmentsitems/lookup-departmentsdepartments
lookupLocationsitems/lookup-locationslocations
lookupSublocationsitems/lookup-sublocationssublocations

Note: Verify the exact endpoint paths and field names by reading the current implementation of each function before applying the table above.


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).

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 decodeJWTPayload function returns null on 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