Standardize LMS API/action error contract
Problem¶
LMS uses at least three different error shapes today:
- Route handlers using
{ error: "unauthorized" }/{ error: "forbidden" }(e.g. impersonate routes, asset proxies). - The CMS revalidate handler defines its own
userSafeError(status, code)helper inline (see apps/lms/src/app/api/cms/revalidate/route.ts lines 31–37). - Server actions returning
{ ok: false, errorKey: "rate_limited" }discriminated unions.
There is no shared handleApiError helper. CMS, by contrast, has apps/cms/src/lib/api/errors.ts with ApiError + handleApiError() and a contract test that locks { error, code, issues? }.
Proposal¶
- New module
apps/lms/src/lib/api/errors.tswith:ApiErrorclass (status, code, optional details/issues).handleApiError(error: unknown): Responsereturning{ error, code, issues? }.- Typed helpers:
unauthorized(),forbidden(),notFound(),validationError(zodError),rateLimited(retryAfter?),serverError().
- Define a shared
ErrorCodeenum (orconstunion) and use the same codes across both transports. - Server action result type: Map between action results and API responses via small adapters; do not unify the transports (per standing decision).
- Migrate all LMS route handlers under apps/lms/src/app/api/ to use the new helpers.
- Migrate all
actions.tsfiles under apps/lms/src/app/ to the newActionResulttype and shared codes. - Add a contract test
apps/lms/src/lib/api/__tests__/error-response-contract.test.tsparallel to the CMS version.
Acceptance criteria¶
- All LMS route handlers return
{ error, code, issues? }. - All LMS server actions return
ActionResult<T>with the shared code enum. - Contract test asserts shape for at least one route per HTTP status (401/403/404/422/429/500).
- No
Response.json({ error: "..." })ad-hoc shapes remain in LMS app code (rg sweep). -
npm run checkpasses; existing tests updated.
Out of scope¶
- Converging CMS and LMS error helpers into one package (handled by T-006 once both apps are stable).
- Changing the underlying mutation transport (server actions stay server actions per standing decision).
- i18n message catalogs for error codes (separate ticket if needed).
Notes / decisions log¶
- 2026-05-24: Ticket created from monorepo audit.
- 2026-05-25: Implemented
apps/lms/src/lib/api/*(SCREAMING_SNAKEErrorCode,errorCodeToI18nKeymapper preserving snake_case i18n keys). Migrated API routes, server actions, credentials/oauth/token consume, and UI consumers. Documented in apps/lms/docs/reference/api-errors.md.