Skip to content

Commit 7dab24a

Browse files
committed
refactor(data): enhance API responses with request metadata
- Add RequestId to handlers and include in error logs - Implement ResponseMetadata in GET and POST responses - Use UTC timestamp in ResponseMetadata for consistency - Update error handling to provide more detailed server logs - Reorganize imports and remove duplicates
1 parent 7b4b1df commit 7dab24a

File tree

1 file changed

+79
-39
lines changed

1 file changed

+79
-39
lines changed

routes/api/v1/data/index.dart

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
//
2-
// ignore_for_file: lines_longer_than_80_chars, avoid_catches_without_on_clauses, avoid_catching_errors
3-
41
import 'dart:io';
52

63
import 'package:dart_frog/dart_frog.dart';
7-
import 'package:ht_api/src/registry/model_registry.dart';
8-
import 'package:dart_frog/dart_frog.dart';
4+
// Import RequestId from the middleware file where it's defined
95
import 'package:ht_api/src/registry/model_registry.dart';
106
import 'package:ht_data_repository/ht_data_repository.dart';
117
import 'package:ht_http_client/ht_http_client.dart'; // Import exceptions
12-
import 'package:ht_shared/ht_shared.dart'; // Import models, SuccessApiResponse, PaginatedResponse
8+
// Import models, SuccessApiResponse, PaginatedResponse, ResponseMetadata
9+
import 'package:ht_shared/ht_shared.dart';
10+
11+
import '../../../_middleware.dart';
1312

1413
/// Handles requests for the /api/v1/data collection endpoint.
1514
/// Dispatches requests to specific handlers based on the HTTP method.
@@ -18,13 +17,18 @@ Future<Response> onRequest(RequestContext context) async {
1817
final modelName = context.read<String>();
1918
// Read ModelConfig for fromJson (needed for POST)
2019
final modelConfig = context.read<ModelConfig<dynamic>>();
20+
// Read the unique RequestId provided by the root middleware
21+
// Note: This assumes RequestId is always provided by `routes/_middleware.dart`
22+
final requestId = context.read<RequestId>().id;
2123

2224
try {
2325
switch (context.request.method) {
2426
case HttpMethod.get:
25-
return await _handleGet(context, modelName);
27+
// Pass requestId down to the handler
28+
return await _handleGet(context, modelName, requestId);
2629
case HttpMethod.post:
27-
return await _handlePost(context, modelName, modelConfig);
30+
// Pass requestId down to the handler
31+
return await _handlePost(context, modelName, modelConfig, requestId);
2832
// Add cases for other methods if needed in the future
2933
default:
3034
// Methods not allowed on the collection endpoint
@@ -38,8 +42,9 @@ Future<Response> onRequest(RequestContext context) async {
3842
rethrow;
3943
} catch (e, stackTrace) {
4044
// Handle any other unexpected errors locally (e.g., provider resolution)
45+
// Include requestId in the server log for easier debugging
4146
print(
42-
'Unexpected error in /data/index.dart handler: $e\n$stackTrace',
47+
'[ReqID: $requestId] Unexpected error in /data/index.dart handler: $e\n$stackTrace',
4348
);
4449
return Response(
4550
statusCode: HttpStatus.internalServerError,
@@ -50,8 +55,12 @@ Future<Response> onRequest(RequestContext context) async {
5055

5156
// --- GET Handler ---
5257
/// Handles GET requests: Retrieves all items for the specified model
53-
/// (with optional query/pagination).
54-
Future<Response> _handleGet(RequestContext context, String modelName) async {
58+
/// (with optional query/pagination). Includes request metadata in response.
59+
Future<Response> _handleGet(
60+
RequestContext context,
61+
String modelName,
62+
String requestId, // Receive requestId
63+
) async {
5564
// Read query parameters
5665
final queryParams = context.request.uri.queryParameters;
5766
final startAfterId = queryParams['startAfterId'];
@@ -112,8 +121,9 @@ Future<Response> _handleGet(RequestContext context, String modelName) async {
112121
}
113122
} catch (e) {
114123
// Catch potential provider errors during context.read within this handler
124+
// Include requestId in the server log
115125
print(
116-
'Error reading repository provider for model "$modelName" in _handleGet: $e',
126+
'[ReqID: $requestId] Error reading repository provider for model "$modelName" in _handleGet: $e',
117127
);
118128
return Response(
119129
statusCode: HttpStatus.internalServerError,
@@ -122,10 +132,16 @@ Future<Response> _handleGet(RequestContext context, String modelName) async {
122132
);
123133
}
124134

125-
// Wrap the PaginatedResponse in SuccessApiResponse and serialize
135+
// Create metadata including the request ID and current timestamp
136+
final metadata = ResponseMetadata(
137+
requestId: requestId,
138+
timestamp: DateTime.now().toUtc(), // Use UTC for consistency
139+
);
140+
141+
// Wrap the PaginatedResponse in SuccessApiResponse with metadata
126142
final successResponse = SuccessApiResponse<PaginatedResponse<dynamic>>(
127143
data: paginatedResponse,
128-
// metadata: ResponseMetadata(timestamp: DateTime.now()), // Optional
144+
metadata: metadata, // Include the created metadata
129145
);
130146

131147
// Need to provide the correct toJsonT for PaginatedResponse
@@ -135,15 +151,18 @@ Future<Response> _handleGet(RequestContext context, String modelName) async {
135151
),
136152
);
137153

154+
// Return 200 OK with the wrapped and serialized response
138155
return Response.json(body: responseJson);
139156
}
140157

141158
// --- POST Handler ---
142159
/// Handles POST requests: Creates a new item for the specified model.
160+
/// Includes request metadata in response.
143161
Future<Response> _handlePost(
144162
RequestContext context,
145163
String modelName,
146164
ModelConfig modelConfig,
165+
String requestId, // Receive requestId
147166
) async {
148167
final requestBody = await context.request.json() as Map<String, dynamic>?;
149168
if (requestBody == null) {
@@ -159,13 +178,15 @@ Future<Response> _handlePost(
159178
newItem = modelConfig.fromJson(requestBody);
160179
} on TypeError catch (e) {
161180
// Catch errors during deserialization (e.g., missing required fields)
162-
print('Deserialization TypeError in POST /data: $e');
181+
// Include requestId in the server log
182+
print('[ReqID: $requestId] Deserialization TypeError in POST /data: $e');
163183
return Response.json(
164184
statusCode: HttpStatus.badRequest, // 400
165185
body: {
166186
'error': {
167187
'code': 'INVALID_REQUEST_BODY',
168-
'message': 'Invalid request body: Missing or invalid required field(s).',
188+
'message':
189+
'Invalid request body: Missing or invalid required field(s).',
169190
// 'details': e.toString(), // Optional: Include details in dev
170191
},
171192
},
@@ -176,39 +197,58 @@ Future<Response> _handlePost(
176197
dynamic createdItem; // Use dynamic
177198
// Repository exceptions (like BadRequestException from create) will propagate
178199
// up to the main onRequest try/catch and be re-thrown to the middleware.
179-
switch (modelName) {
180-
case 'headline':
181-
final repo = context.read<HtDataRepository<Headline>>();
182-
createdItem = await repo.create(newItem as Headline);
183-
case 'category':
184-
final repo = context.read<HtDataRepository<Category>>();
185-
createdItem = await repo.create(newItem as Category);
186-
case 'source':
187-
final repo = context.read<HtDataRepository<Source>>();
188-
createdItem = await repo.create(newItem as Source);
189-
case 'country':
190-
final repo = context.read<HtDataRepository<Country>>();
191-
createdItem = await repo.create(newItem as Country);
192-
default:
193-
// This case should ideally be caught by middleware, but added for safety
194-
return Response(
195-
statusCode: HttpStatus.internalServerError,
196-
body:
197-
'Internal Server Error: Unsupported model type "$modelName" reached handler.',
198-
);
200+
try {
201+
switch (modelName) {
202+
case 'headline':
203+
final repo = context.read<HtDataRepository<Headline>>();
204+
createdItem = await repo.create(newItem as Headline);
205+
case 'category':
206+
final repo = context.read<HtDataRepository<Category>>();
207+
createdItem = await repo.create(newItem as Category);
208+
case 'source':
209+
final repo = context.read<HtDataRepository<Source>>();
210+
createdItem = await repo.create(newItem as Source);
211+
case 'country':
212+
final repo = context.read<HtDataRepository<Country>>();
213+
createdItem = await repo.create(newItem as Country);
214+
default:
215+
// This case should ideally be caught by middleware, but added for safety
216+
return Response(
217+
statusCode: HttpStatus.internalServerError,
218+
body:
219+
'Internal Server Error: Unsupported model type "$modelName" reached handler.',
220+
);
221+
}
222+
} catch (e) {
223+
// Catch potential provider errors during context.read within this handler
224+
// Include requestId in the server log
225+
print(
226+
'[ReqID: $requestId] Error reading repository provider for model "$modelName" in _handlePost: $e',
227+
);
228+
return Response(
229+
statusCode: HttpStatus.internalServerError,
230+
body:
231+
'Internal Server Error: Could not resolve repository for model "$modelName".',
232+
);
199233
}
200234

201-
// Wrap the created item in SuccessApiResponse and serialize
235+
// Create metadata including the request ID and current timestamp
236+
final metadata = ResponseMetadata(
237+
requestId: requestId,
238+
timestamp: DateTime.now().toUtc(), // Use UTC for consistency
239+
);
240+
241+
// Wrap the created item in SuccessApiResponse with metadata
202242
final successResponse = SuccessApiResponse<dynamic>(
203243
data: createdItem,
204-
// metadata: ResponseMetadata(timestamp: DateTime.now()), // Optional
244+
metadata: metadata, // Include the created metadata
205245
);
206246

207247
// Provide the correct toJsonT for the specific model type
208248
final responseJson = successResponse.toJson(
209249
(item) => (item as dynamic).toJson(), // Assuming all models have toJson
210250
);
211251

212-
// Return the serialized response
252+
// Return 201 Created with the wrapped and serialized response
213253
return Response.json(statusCode: HttpStatus.created, body: responseJson);
214254
}

0 commit comments

Comments
 (0)