When auth errors return { message: '...' }, validation errors return { errors: [...] }, and not-found errors return raw strings, consumers must write a different parser for each error type. This is not just inconvenient — it makes automated error handling (retry logic, error monitoring, user-facing messages) impossible to build generically. iso-25010:2011 compatibility.interoperability and maintainability.modifiability both require a stable, machine-readable error contract. Inconsistent errors are also a debugging liability: different shapes in different production logs make tracing failures significantly harder.
Critical because inconsistent error shapes make automated error handling and observability impossible, and every new error type added to the API risks introducing yet another shape.
Define one error structure and use it for every error response across every endpoint. Create a helper to enforce it:
interface ApiError {
error: {
code: string // machine-readable: "NOT_FOUND", "VALIDATION_ERROR"
message: string // human-readable description
details?: unknown // field-level detail for validation errors
}
}
function apiError(code: string, message: string, status: number, details?: unknown) {
return Response.json({ error: { code, message, details } }, { status })
}
// In every handler:
return apiError('NOT_FOUND', 'User not found', 404)
return apiError('VALIDATION_ERROR', 'Invalid request', 400, zodError.issues)
return apiError('INTERNAL_ERROR', 'Something went wrong', 500)
The code field is the key — it lets consumers branch on machine-readable values rather than parsing human-readable strings. See the status-codes check for HTTP code consistency.
ID: api-design.contract-quality.error-schema
Severity: critical
What to look for: Examine error responses across all endpoints. Check whether every error -- validation errors, not-found errors, auth errors, server errors -- follows the same structural shape. Common patterns: { error: { code: "NOT_FOUND", message: "User not found" } } or { error: "message" } or { message: "...", statusCode: 404 }. Look for inconsistency: some endpoints return { error: "..." }, others return { message: "..." }, others return raw strings or HTML error pages. Also check: do error responses include a machine-readable error code (not just the HTTP status)?
Pass criteria: Enumerate all distinct error response shapes across the codebase. 100% of error responses across all endpoints must use the same structure. The structure includes at minimum a machine-readable code and a human-readable message. Validation errors include field-level detail. Report the count of distinct error shapes found even on pass.
Fail criteria: Error response shapes differ across endpoints -- some return { error: "..." }, others return { message: "..." }, others return different structures. Or error responses are raw strings, HTML, or vary in structure by error type. Must not pass when more than 1 distinct error shape exists across the API.
Skip (N/A) when: Never skip -- every API returns errors.
Detail on fail: Describe the inconsistency (e.g., "Auth errors return { message: '...' }, validation errors return { errors: [...] }, not-found returns { error: '...' }, and server errors return raw Internal Server Error string. 4 different error shapes across 12 endpoints."). Max 500 chars.
Cross-reference: For HTTP status code usage consistency, see the status-codes check in the Developer Ergonomics category below.
Remediation: Define a single error response structure and use it for ALL errors:
// Consistent error shape:
interface ApiError {
error: {
code: string // machine-readable: "NOT_FOUND", "VALIDATION_ERROR"
message: string // human-readable description
details?: unknown // optional field-level detail for validation errors
}
}
// Helper function:
function apiError(code: string, message: string, status: number, details?: unknown) {
return Response.json({ error: { code, message, details } }, { status })
}
// Usage in every handler:
return apiError('NOT_FOUND', 'User not found', 404)
return apiError('VALIDATION_ERROR', 'Invalid request', 400, zodError.issues)
return apiError('INTERNAL_ERROR', 'Something went wrong', 500)