All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m24s
5.5 KiB
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:
- Some endpoints return raw data arrays (
[{...}, {...}]) - Some return wrapped objects (
{ data: [...] }) - Pagination is handled inconsistently (some use
page/limit, others useoffset/count) - Error responses vary in structure between middleware and route handlers
- 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
-
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?)
-
Error Handler Integration: Update
errorHandler.tsto use the standard error format -
Gradual Migration: Apply to new endpoints immediately, migrate existing endpoints incrementally
-
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.tswith helper functions (sendSuccess,sendPaginated,sendError,sendNoContent,sendMessage,calculatePagination) - ✅ Created
src/types/api.tswith response type definitions (ApiSuccessResponse,ApiErrorResponse,PaginationMeta,ErrorCode) - ✅ Updated
src/middleware/errorHandler.tsto use standard error format - ✅ Migrated all route files to use standardized responses:
health.routes.tsflyer.routes.tsdeals.routes.tsbudget.routes.tspersonalization.routes.tsprice.routes.tsreactions.routes.tsstats.routes.tssystem.routes.tsgamification.routes.tsrecipe.routes.tsauth.routes.tsuser.routes.tsadmin.routes.tsai.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"
// }
// }