Skip to content

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

  1. New module apps/lms/src/lib/api/errors.ts with:
    • ApiError class (status, code, optional details/issues).
    • handleApiError(error: unknown): Response returning { error, code, issues? }.
    • Typed helpers: unauthorized(), forbidden(), notFound(), validationError(zodError), rateLimited(retryAfter?), serverError().
  2. Define a shared ErrorCode enum (or const union) and use the same codes across both transports.
  3. Server action result type:
    export type ActionResult<T> =
      | { ok: true; data: T }
      | { ok: false; code: ErrorCode; message?: string; issues?: ZodIssue[] };
    
    Map between action results and API responses via small adapters; do not unify the transports (per standing decision).
  4. Migrate all LMS route handlers under apps/lms/src/app/api/ to use the new helpers.
  5. Migrate all actions.ts files under apps/lms/src/app/ to the new ActionResult type and shared codes.
  6. Add a contract test apps/lms/src/lib/api/__tests__/error-response-contract.test.ts parallel 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 check passes; 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_SNAKE ErrorCode, errorCodeToI18nKey mapper 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.