Skip to content

Refactor improve router response handlers #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions lib/src/config/app_dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class AppDependencies {
late final HtDataRepository<User> userRepository;
late final HtDataRepository<UserAppSettings> userAppSettingsRepository;
late final HtDataRepository<UserContentPreferences>
userContentPreferencesRepository;
userContentPreferencesRepository;
late final HtDataRepository<RemoteConfig> remoteConfigRepository;
late final HtEmailRepository emailRepository;

Expand Down Expand Up @@ -134,12 +134,12 @@ class AppDependencies {
);
final userContentPreferencesClient =
HtDataMongodb<UserContentPreferences>(
connectionManager: _mongoDbConnectionManager,
modelName: 'user_content_preferences',
fromJson: UserContentPreferences.fromJson,
toJson: (item) => item.toJson(),
logger: Logger('HtDataMongodb<UserContentPreferences>'),
);
connectionManager: _mongoDbConnectionManager,
modelName: 'user_content_preferences',
fromJson: UserContentPreferences.fromJson,
toJson: (item) => item.toJson(),
logger: Logger('HtDataMongodb<UserContentPreferences>'),
);
final remoteConfigClient = HtDataMongodb<RemoteConfig>(
connectionManager: _mongoDbConnectionManager,
modelName: 'remote_configs',
Expand All @@ -154,12 +154,13 @@ class AppDependencies {
sourceRepository = HtDataRepository(dataClient: sourceClient);
countryRepository = HtDataRepository(dataClient: countryClient);
userRepository = HtDataRepository(dataClient: userClient);
userAppSettingsRepository =
HtDataRepository(dataClient: userAppSettingsClient);
userContentPreferencesRepository =
HtDataRepository(dataClient: userContentPreferencesClient);
remoteConfigRepository =
HtDataRepository(dataClient: remoteConfigClient);
userAppSettingsRepository = HtDataRepository(
dataClient: userAppSettingsClient,
);
userContentPreferencesRepository = HtDataRepository(
dataClient: userContentPreferencesClient,
);
remoteConfigRepository = HtDataRepository(dataClient: remoteConfigClient);

final emailClient = HtEmailInMemoryClient(
logger: Logger('HtEmailInMemoryClient'),
Expand All @@ -176,8 +177,7 @@ class AppDependencies {
uuidGenerator: const Uuid(),
log: Logger('JwtAuthTokenService'),
);
verificationCodeStorageService =
InMemoryVerificationCodeStorageService();
verificationCodeStorageService = InMemoryVerificationCodeStorageService();
permissionService = const PermissionService();
authService = AuthService(
userRepository: userRepository,
Expand Down Expand Up @@ -219,4 +219,4 @@ class AppDependencies {
_isInitialized = false;
_log.info('Application dependencies disposed.');
}
}
}
8 changes: 6 additions & 2 deletions lib/src/config/environment_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,19 @@ abstract final class EnvironmentConfig {
env.load([envPath]); // Load variables from the found .env file
return env; // Return immediately upon finding
} else {
_log.warning('pubspec.yaml found, but no .env in the same directory.');
_log.warning(
'pubspec.yaml found, but no .env in the same directory.',
);
break; // Stop searching since pubspec.yaml should contain .env
}
}
currentDir = currentDir.parent; // Move to the parent directory
_log.finer('Moving up to parent directory: ${currentDir.path}');
}
// If loop completes without returning, .env was not found
_log.warning('.env not found by searching. Falling back to default load().');
_log.warning(
'.env not found by searching. Falling back to default load().',
);
env.load(); // Fallback to default load
return env; // Return even if fallback
}
Expand Down
42 changes: 42 additions & 0 deletions lib/src/helpers/response_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:ht_api/src/models/request_id.dart';
import 'package:ht_shared/ht_shared.dart';

/// A utility class to simplify the creation of standardized API responses.
abstract final class ResponseHelper {
/// Creates a standardized success JSON response.
///
/// This helper encapsulates the boilerplate of creating metadata, wrapping the
/// payload in a [SuccessApiResponse], and serializing it to JSON.
///
/// - [context]: The request context, used to read the `RequestId`.
/// - [data]: The payload to be included in the response.
/// - [toJsonT]: A function that knows how to serialize the [data] payload.
/// This is necessary because of Dart's generics. For a simple object, this
/// would be `(data) => data.toJson()`. For a paginated list, it would be
/// `(paginated) => paginated.toJson((item) => item.toJson())`.
/// - [statusCode]: The HTTP status code for the response. Defaults to 200 OK.
static Response success<T>({
required RequestContext context,
required T data,
required Map<String, dynamic> Function(T data) toJsonT,
int statusCode = HttpStatus.ok,
}) {
final metadata = ResponseMetadata(
requestId: context.read<RequestId>().id,
timestamp: DateTime.now().toUtc(),
);

final responsePayload = SuccessApiResponse<T>(
data: data,
metadata: metadata,
);

return Response.json(
statusCode: statusCode,
body: responsePayload.toJson(toJsonT),
);
}
}
11 changes: 8 additions & 3 deletions lib/src/middlewares/error_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ Middleware errorHandler() {
} on CheckedFromJsonException catch (e, stackTrace) {
// Handle json_serializable validation errors. These are client errors.
final field = e.key ?? 'unknown';
final message = 'Invalid request body: Field "$field" has an '
final message =
'Invalid request body: Field "$field" has an '
'invalid value or is missing. ${e.message}';
print('CheckedFromJsonException Caught: $e\n$stackTrace');
return _jsonErrorResponse(
Expand All @@ -50,7 +51,9 @@ Middleware errorHandler() {
print('Unhandled Exception Caught: $e\n$stackTrace');
return _jsonErrorResponse(
statusCode: HttpStatus.internalServerError, // 500
exception: const UnknownException('An unexpected internal server error occurred.'),
exception: const UnknownException(
'An unexpected internal server error occurred.',
),
context: context,
);
}
Expand Down Expand Up @@ -137,7 +140,9 @@ Response _jsonErrorResponse({

return Response.json(
statusCode: statusCode,
body: {'error': {'code': errorCode, 'message': exception.message}},
body: {
'error': {'code': errorCode, 'message': exception.message},
},
headers: headers,
);
}
39 changes: 39 additions & 0 deletions lib/src/models/request_id.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// {@template request_id}
/// A wrapper class holding a unique identifier (UUID v4) generated for each
/// incoming HTTP request.
///
/// **Purpose:**
/// The primary role of this ID is **traceability for logging and debugging**.
/// It allows developers to follow the entire lifecycle of a *single request*
/// through various middleware, route handlers, repository calls, and potential
/// external service interactions by searching logs for this specific ID.
/// If an error occurs during a request, this ID provides a way to isolate all
/// related log entries for that specific transaction, simplifying debugging.
///
/// **Scope:**
/// - The ID is **transient** for the request itself; it exists only during the
/// request-response cycle.
/// - It is **not persisted** in the main application database alongside models
/// like Headlines or Categories.
/// - Its value lies in being included in **persistent logs**.
///
/// **Distinction from other IDs:**
/// - **User ID:** Identifies the authenticated user making the request. Often
/// logged alongside the `request_id` for user-specific debugging.
/// - **Session ID:** Tracks a user's session across multiple requests.
/// - **Correlation ID:** Often generated by the *client* and passed in headers
/// to link related requests initiated by the client for a larger workflow.
///
/// **Implementation:**
/// This class ensures type safety when providing and reading the request ID
/// from the Dart Frog context using `context.provide<RequestId>` and
/// `context.read<RequestId>()`. This prevents potential ambiguity if other raw
/// strings were provided into the context.
/// {@endtemplate}
class RequestId {
/// {@macro request_id}
const RequestId(this.id);

/// The unique identifier string (UUID v4).
final String id;
}
19 changes: 11 additions & 8 deletions lib/src/services/auth_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ class AuthService {
required HtEmailRepository emailRepository,
required HtDataRepository<UserAppSettings> userAppSettingsRepository,
required HtDataRepository<UserContentPreferences>
userContentPreferencesRepository,
userContentPreferencesRepository,
required PermissionService permissionService,
required Uuid uuidGenerator,
required Logger log,
}) : _userRepository = userRepository,
_authTokenService = authTokenService,
_verificationCodeStorageService = verificationCodeStorageService,
_permissionService = permissionService,
_permissionService = permissionService,
_emailRepository = emailRepository,
_userAppSettingsRepository = userAppSettingsRepository,
_userContentPreferencesRepository = userContentPreferencesRepository,
Expand All @@ -43,7 +43,7 @@ class AuthService {
final HtEmailRepository _emailRepository;
final HtDataRepository<UserAppSettings> _userAppSettingsRepository;
final HtDataRepository<UserContentPreferences>
_userContentPreferencesRepository;
_userContentPreferencesRepository;
final PermissionService _permissionService;
final Logger _log;
final Uuid _uuid;
Expand Down Expand Up @@ -83,7 +83,10 @@ class AuthService {
throw const UnauthorizedException(
'This email address is not registered for dashboard access.',
);
} else if (!_permissionService.hasPermission(user, Permissions.dashboardLogin)) {
} else if (!_permissionService.hasPermission(
user,
Permissions.dashboardLogin,
)) {
_log.warning(
'Dashboard login failed: User ${user.id} lacks required permission (${Permissions.dashboardLogin}).',
);
Expand Down Expand Up @@ -273,7 +276,9 @@ class AuthService {
rethrow;
} catch (e, s) {
_log.severe(
'Unexpected error during user lookup/creation for $email: $e', e, s,
'Unexpected error during user lookup/creation for $email: $e',
e,
s,
);
throw const OperationFailedException('Failed to process user account.');
}
Expand Down Expand Up @@ -652,9 +657,7 @@ class AuthService {
/// Re-throws any [HtHttpException] from the repository.
Future<User?> _findUserByEmail(String email) async {
try {
final response = await _userRepository.readAll(
filter: {'email': email},
);
final response = await _userRepository.readAll(filter: {'email': email});
if (response.items.isNotEmpty) {
return response.items.first;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/services/database_seeding_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class DatabaseSeedingService {
// Filter by the specific, deterministic _id.
'filter': {'_id': objectId},
// Set the fields of the document.
'update': {'\$set': document},
'update': {r'$set': document},
'upsert': true,
},
});
Expand Down
43 changes: 1 addition & 42 deletions routes/_middleware.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dart_frog/dart_frog.dart';
import 'package:ht_api/src/config/app_dependencies.dart';
import 'package:ht_api/src/middlewares/error_handler.dart';
import 'package:ht_api/src/models/request_id.dart';
import 'package:ht_api/src/rbac/permission_service.dart';
import 'package:ht_api/src/registry/model_registry.dart';
import 'package:ht_api/src/services/auth_service.dart';
Expand All @@ -15,48 +16,6 @@ import 'package:ht_shared/ht_shared.dart';
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';

// --- Request ID Wrapper ---

/// {@template request_id}
/// A wrapper class holding a unique identifier (UUID v4) generated for each
/// incoming HTTP request.
///
/// **Purpose:**
/// The primary role of this ID is **traceability for logging and debugging**.
/// It allows developers to follow the entire lifecycle of a *single request*
/// through various middleware, route handlers, repository calls, and potential
/// external service interactions by searching logs for this specific ID.
/// If an error occurs during a request, this ID provides a way to isolate all
/// related log entries for that specific transaction, simplifying debugging.
///
/// **Scope:**
/// - The ID is **transient** for the request itself; it exists only during the
/// request-response cycle.
/// - It is **not persisted** in the main application database alongside models
/// like Headlines or Categories.
/// - Its value lies in being included in **persistent logs**.
///
/// **Distinction from other IDs:**
/// - **User ID:** Identifies the authenticated user making the request. Often
/// logged alongside the `request_id` for user-specific debugging.
/// - **Session ID:** Tracks a user's session across multiple requests.
/// - **Correlation ID:** Often generated by the *client* and passed in headers
/// to link related requests initiated by the client for a larger workflow.
///
/// **Implementation:**
/// This class ensures type safety when providing and reading the request ID
/// from the Dart Frog context using `context.provide<RequestId>` and
/// `context.read<RequestId>()`. This prevents potential ambiguity if other raw
/// strings were provided into the context.
/// {@endtemplate}
class RequestId {
/// {@macro request_id}
const RequestId(this.id);

/// The unique identifier string (UUID v4).
final String id;
}

// --- Middleware Definition ---
final _log = Logger('RootMiddleware');

Expand Down
22 changes: 5 additions & 17 deletions routes/api/v1/auth/anonymous.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:ht_api/src/helpers/response_helper.dart';
import 'package:ht_api/src/services/auth_service.dart';
import 'package:ht_shared/ht_shared.dart';

import '../../../_middleware.dart';

/// Handles POST requests to `/api/v1/auth/anonymous`.
///
/// Creates a new anonymous user and returns the User object along with an
Expand All @@ -29,22 +28,11 @@ Future<Response> onRequest(RequestContext context) async {
token: result.token,
);

// Create metadata, including the requestId from the context.
final metadata = ResponseMetadata(
requestId: context.read<RequestId>().id,
timestamp: DateTime.now().toUtc(),
);

// Wrap the payload in the standard SuccessApiResponse
final responsePayload = SuccessApiResponse<AuthSuccessResponse>(
// Use the helper to create a standardized success response
return ResponseHelper.success(
context: context,
data: authPayload,
metadata: metadata,
);

// Return 200 OK with the standardized, serialized response
return Response.json(
// Use the toJson method, providing the toJson factory for the inner type
body: responsePayload.toJson((authSuccess) => authSuccess.toJson()),
toJsonT: (data) => data.toJson(),
);
} on HtHttpException catch (_) {
// Let the central errorHandler middleware handle known exceptions
Expand Down
Loading
Loading