Files
flyer-crawler.projectium.com/docs/adr/0028-api-response-standardization.md
Torben Sorensen 3912139273
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m24s
adr-028 and int tests
2026-01-09 12:47:41 -08:00

5.5 KiB

ADR-028: API Response Standardization and Envelope Pattern

Date: 2026-01-09

Status: Implemented

Context

The API currently has inconsistent response formats across different endpoints:

  1. Some endpoints return raw data arrays ([{...}, {...}])
  2. Some return wrapped objects ({ data: [...] })
  3. Pagination is handled inconsistently (some use page/limit, others use offset/count)
  4. Error responses vary in structure between middleware and route handlers
  5. No standard for including metadata (pagination info, request timing, etc.)

This inconsistency creates friction for:

  • Frontend developers who must handle multiple response formats
  • API documentation and client SDK generation
  • Implementing consistent error handling across the application
  • Future API versioning transitions

Decision

We will adopt a standardized response envelope pattern for all API responses.

Success Response Format

interface ApiSuccessResponse<T> {
  success: true;
  data: T;
  meta?: {
    // Pagination (when applicable)
    pagination?: {
      page: number;
      limit: number;
      total: number;
      totalPages: number;
      hasNextPage: boolean;
      hasPrevPage: boolean;
    };
    // Timing
    requestId?: string;
    timestamp?: string;
    duration?: number;
  };
}

Error Response Format

interface ApiErrorResponse {
  success: false;
  error: {
    code: string; // Machine-readable error code (e.g., 'VALIDATION_ERROR')
    message: string; // Human-readable message
    details?: unknown; // Additional context (validation errors, etc.)
  };
  meta?: {
    requestId?: string;
    timestamp?: string;
  };
}

Implementation Approach

  1. Response Helper Functions: Create utility functions in src/utils/apiResponse.ts:

    • sendSuccess(res, data, meta?)
    • sendPaginated(res, data, pagination)
    • sendError(res, code, message, details?, statusCode?)
  2. Error Handler Integration: Update errorHandler.ts to use the standard error format

  3. Gradual Migration: Apply to new endpoints immediately, migrate existing endpoints incrementally

  4. TypeScript Types: Export response types for frontend consumption

Consequences

Positive

  • Consistency: All responses follow a predictable structure
  • Type Safety: Frontend can rely on consistent types
  • Debugging: Request IDs and timestamps aid in issue investigation
  • Pagination: Standardized pagination metadata reduces frontend complexity
  • API Evolution: Envelope pattern makes it easier to add fields without breaking changes

Negative

  • Verbosity: Responses are slightly larger due to envelope overhead
  • Migration Effort: Existing endpoints need updating
  • Learning Curve: Developers must learn and use the helper functions

Implementation Status

What's Implemented

  • Created src/utils/apiResponse.ts with helper functions (sendSuccess, sendPaginated, sendError, sendNoContent, sendMessage, calculatePagination)
  • Created src/types/api.ts with response type definitions (ApiSuccessResponse, ApiErrorResponse, PaginationMeta, ErrorCode)
  • Updated src/middleware/errorHandler.ts to use standard error format
  • Migrated all route files to use standardized responses:
    • health.routes.ts
    • flyer.routes.ts
    • deals.routes.ts
    • budget.routes.ts
    • personalization.routes.ts
    • price.routes.ts
    • reactions.routes.ts
    • stats.routes.ts
    • system.routes.ts
    • gamification.routes.ts
    • recipe.routes.ts
    • auth.routes.ts
    • user.routes.ts
    • admin.routes.ts
    • ai.routes.ts

Error Codes

The following error codes are defined in src/types/api.ts:

Code HTTP Status Description
VALIDATION_ERROR 400 Request validation failed
BAD_REQUEST 400 Malformed request
UNAUTHORIZED 401 Authentication required
FORBIDDEN 403 Insufficient permissions
NOT_FOUND 404 Resource not found
CONFLICT 409 Resource conflict (e.g., duplicate)
RATE_LIMITED 429 Too many requests
PAYLOAD_TOO_LARGE 413 Request body too large
INTERNAL_ERROR 500 Server error
NOT_IMPLEMENTED 501 Feature not yet implemented
SERVICE_UNAVAILABLE 503 Service temporarily unavailable
EXTERNAL_SERVICE_ERROR 502 External service failure

Example Usage

// In a route handler
router.get('/flyers', async (req, res, next) => {
  try {
    const { page = 1, limit = 20 } = req.query;
    const { flyers, total } = await flyerService.getFlyers({ page, limit });

    return sendPaginated(res, flyers, {
      page,
      limit,
      total,
    });
  } catch (error) {
    next(error);
  }
});

// Response:
// {
//   "success": true,
//   "data": [...],
//   "meta": {
//     "pagination": {
//       "page": 1,
//       "limit": 20,
//       "total": 150,
//       "totalPages": 8,
//       "hasNextPage": true,
//       "hasPrevPage": false
//     },
//     "requestId": "abc-123",
//     "timestamp": "2026-01-09T12:00:00.000Z"
//   }
// }