Skip to content

LMS API and action error contract

The LMS uses one machine-readable ErrorCode set for route handlers, server actions, and internal auth helpers. User-facing copy stays in next-intl; codes map to existing message slugs via errorCodeToI18nKey().

Modules

Module Role
src/lib/api/error-codes.ts ERROR_CODES const + ErrorCode type + default HTTP status map
src/lib/api/errors.ts ApiError, factories (unauthorized, forbidden, rateLimited, …), handleApiError()
src/lib/api/action-result.ts ActionResult<T>, actionOk, actionFail, fromZodError, isActionFailure
src/lib/api/i18n.ts errorCodeToI18nKey(code) — SCREAMING_SNAKE → snake_case i18n keys
src/lib/api/adapters.ts actionFailureToApiError() for fetch callers

Contract tests live under src/lib/api/__tests__/.

Route handlers (HTTP)

Failures return JSON:

{ "error": "Human-readable message", "code": "FORBIDDEN", "issues": [] }
  • error — safe for clients; never stack traces or secrets.
  • code — stable ErrorCode (SCREAMING_SNAKE).
  • issues — optional Zod issue list on validation failures.

Wrap handlers with handleApiError() in catch blocks. Throw ApiError or factory helpers for expected failures.

Server actions

Success:

{ ok: true, data: T }

Failure:

{ ok: false, code: ErrorCode, message?: string, issues?: ZodIssue[], retryAfterSeconds?: number }

UI should use errorCodeToI18nKey(result.code) when translating to t(\error.${key}`)` or hard-coded switch keys.

Internal auth

authorizeCredentials, linkOrCreateGoogleUser, and consumeVerificationToken return { ok: false, code: ErrorCode } instead of ad-hoc reason strings. Server actions map these codes into actionFail().

CMS alignment

Shape and helper layout mirror apps/cms/src/lib/api/errors.ts. Shared package extraction is tracked in audit ticket T-006.