TASK22102: slice1 api contract helper + email/admin pilot

This commit is contained in:
2026-03-17 12:50:54 +00:00
parent 7d06bc7bfa
commit 9ae8ce7136
7 changed files with 98 additions and 9 deletions
+1
View File
@@ -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.
+37
View File
@@ -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.
+7 -2
View File
@@ -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;
+7 -2
View File
@@ -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;
+7 -2
View File
@@ -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"
});
});
}
+12 -3
View File
@@ -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);
}
+27
View File
@@ -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);
};