diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 6b28e99d..6dc925c7 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -2,6 +2,7 @@ ## Current development focus (from recent commits) +- API contract consistency rollout (TASK22102), starting with helper-based response normalization in selected email/admin handlers. - Search/case navigation correctness, especially breadcrumb and back-link behavior. - My Portal "view all" and DNS application path handling. - Welsh/English email behavior for specific notification templates. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 2246687d..300c2dec 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1302,3 +1302,40 @@ Validation: Follow-ups: - Continue same small-batch consistency pattern for remaining endpoint handlers where required-input/logging drift is clear. + +--- + +### CL-035: TASK22102 API contract consistency — Slice 1 (response helper + email/admin pilot) + +date: 2026-03-17 +author: Cline +scope: `pages/api/middleware/apiResponse.js`, `pages/api/email/{notify,getmailinglist}.js`, `pages/api/admin/{getnewappeals_api,getlatestdocuments_api}.js`, `memory-bank/*` +type: change +rationale: Start API contract-consistency program with a low-risk pilot slice introducing shared response helpers and replacing raw error passthrough in selected email/admin handlers. +impact: Improves response contract consistency and prevents raw error payload leakage in pilot handlers while preserving existing success-body compatibility. +status: completed + +Summary: + +- Added shared API response helper module: + - `pages/api/middleware/apiResponse.js` + - `respondSuccess(res, data, status)` + - `respondError(res, { status, code, message, details })` +- Migrated first slice handlers to helper-based responses: + - `pages/api/email/notify.js` + - `pages/api/email/getmailinglist.js` + - `pages/api/admin/getnewappeals_api.js` + - `pages/api/admin/getlatestdocuments_api.js` +- Contract updates in slice: + - replaced `res.status(400).json(error)` with structured error envelope + - replaced direct `res.status(200).json(...)` with `respondSuccess(...)` + - kept success payload shape as existing data object for non-breaking rollout + +Validation: + +- `npx next lint --file pages/api/middleware/apiResponse.js --file pages/api/email/notify.js --file pages/api/email/getmailinglist.js --file pages/api/admin/getnewappeals_api.js --file pages/api/admin/getlatestdocuments_api.js` -> pass (no warnings/errors) + +Follow-ups: + +- Slice 2 should migrate remaining email handlers (`getcaseref`, `getall`, `getevents`, `getdocuments`) to helper responses. +- After email/admin completion, expand to selected high-risk file handlers using same non-breaking helper model. diff --git a/pages/api/admin/getlatestdocuments_api.js b/pages/api/admin/getlatestdocuments_api.js index d9b6b622..686352b3 100644 --- a/pages/api/admin/getlatestdocuments_api.js +++ b/pages/api/admin/getlatestdocuments_api.js @@ -22,6 +22,7 @@ import { azureHeadersPagedCustom } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const WORDKEY = process.env.HASHKEY; const WEBAPI_URL = @@ -154,11 +155,15 @@ export default async function ApiProxy(req, res) { _.has(data, "@odata.nextLink") == true && ((dataStr = JSON.stringify(data["@odata.nextLink"])), (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); - res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "ADMIN_LATEST_DOCS_FETCH_FAILED", + message: "Failed to fetch latest documents" + }); }); return apiResponse; diff --git a/pages/api/admin/getnewappeals_api.js b/pages/api/admin/getnewappeals_api.js index 2ab64f48..595ed467 100644 --- a/pages/api/admin/getnewappeals_api.js +++ b/pages/api/admin/getnewappeals_api.js @@ -22,6 +22,7 @@ import { azureHeadersPagedCustom } from "../../../actions/core/headers"; import { getToken } from "../../../actions/core/token"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const WEBAPI_URL = process.env.RELAY_ROOT || @@ -64,11 +65,15 @@ export default async function ApiProxy(req, res) { _.has(data, "@odata.nextLink") == true && ((dataStr = JSON.stringify(data["@odata.nextLink"])), (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); - res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "ADMIN_NEW_APPEALS_FETCH_FAILED", + message: "Failed to fetch new appeals" + }); }); return apiResponse; diff --git a/pages/api/email/getmailinglist.js b/pages/api/email/getmailinglist.js index 3aaebd3d..44ff7297 100644 --- a/pages/api/email/getmailinglist.js +++ b/pages/api/email/getmailinglist.js @@ -5,6 +5,7 @@ import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const WORDKEY = process.env.HASHKEY; @@ -40,10 +41,14 @@ export default async function ApiProxy(req, res) { const flattenedResults = data.value.map(flattenWatchlistEntry); - res.status(200).json(flattenedResults); + return respondSuccess(res, flattenedResults); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "MAILING_LIST_FETCH_FAILED", + message: "Failed to fetch mailing list" + }); }); } diff --git a/pages/api/email/notify.js b/pages/api/email/notify.js index c74edcc4..a9cef216 100644 --- a/pages/api/email/notify.js +++ b/pages/api/email/notify.js @@ -28,13 +28,18 @@ import { consoleLogger, redactSensitive } from "../../../actions/core/logger"; import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards"; import { getPreferredLanguage } from "../../../actions/services/accountService"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; export default async function ApiProxy(req, res) { var data = req.body; const emailAddress = sanitizeString(data?.emailAddress); if (!isNonEmptyString(emailAddress)) { - return res.status(400).json({ error: "emailAddress is required" }); + return respondError(res, { + status: 400, + code: "EMAIL_ADDRESS_REQUIRED", + message: "emailAddress is required" + }); } data.emailAddress = emailAddress; @@ -71,11 +76,15 @@ export default async function ApiProxy(req, res) { }) .then((response) => { //console.log("thisis the response", response); - return res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { consoleLogger(error); - return res.status(400).json(error); + return respondError(res, { + status: 400, + code: "EMAIL_NOTIFY_FAILED", + message: "Failed to send notify email" + }); }); //return res.status(200).json(data); } diff --git a/pages/api/middleware/apiResponse.js b/pages/api/middleware/apiResponse.js new file mode 100644 index 00000000..d6163e4f --- /dev/null +++ b/pages/api/middleware/apiResponse.js @@ -0,0 +1,27 @@ +export const respondSuccess = (res, data, status = 200) => { + return res.status(status).json(data); +}; + +export const respondError = ( + res, + { + status = 500, + code = "INTERNAL_SERVER_ERROR", + message = "Request failed", + details + } = {} +) => { + const payload = { + success: false, + error: { + code, + message + } + }; + + if (typeof details !== "undefined") { + payload.error.details = details; + } + + return res.status(status).json(payload); +};