All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m24s
178 lines
5.5 KiB
Markdown
178 lines
5.5 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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"
|
|
// }
|
|
// }
|
|
```
|