From 75d993f83a004dd84476ba12bfdb7d9abe4ff0fc Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Wed, 22 Apr 2026 08:14:32 +0200 Subject: [PATCH] refactor: Clean up ApiClient code structure and improve readability --- flutter/lib/core/api/api_client.dart | 269 +++++++++++++-------------- 1 file changed, 130 insertions(+), 139 deletions(-) diff --git a/flutter/lib/core/api/api_client.dart b/flutter/lib/core/api/api_client.dart index 718de24c..966a86f9 100644 --- a/flutter/lib/core/api/api_client.dart +++ b/flutter/lib/core/api/api_client.dart @@ -1,140 +1,131 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; - -import 'api_exception.dart'; - -/// Platform-neutral HTTP client. -/// API base URL is injected at build time via --dart-define=API_BASE_URL. -/// Default is same-origin '/api' to avoid mixed-content on HTTPS sites. -class ApiClient { - final String baseUrl; - final http.Client _client; - - ApiClient({http.Client? client}) - : baseUrl = const String.fromEnvironment( - 'API_BASE_URL', - defaultValue: '/api', - ), - _client = client ?? http.Client(); - - Map _headers({String? token}) => { - 'Content-Type': 'application/json', - if (token != null) 'Authorization': 'Bearer $token', - }; - - Future getJson(String path, {String? token}) async { - final response = await _client.get( - Uri.parse('$baseUrl$path'), - headers: _headers(token: token), - ); - return _decodeOrNull(_guardResponse(response)); - } - - Future postJson( - String path, { - Object? body, - String? token, - }) async { - final response = await _client.post( - Uri.parse('$baseUrl$path'), - headers: _headers(token: token), - body: body == null ? null : jsonEncode(body), - ); - return _decodeOrNull(_guardResponse(response)); - } - - Future putJson( - String path, { - Object? body, - String? token, - }) async { - final response = await _client.put( - Uri.parse('$baseUrl$path'), - headers: _headers(token: token), - body: body == null ? null : jsonEncode(body), - ); - return _decodeOrNull(_guardResponse(response)); - } - - Future patchJson( - String path, { - Object? body, - String? token, - }) async { - final response = await _client.patch( - Uri.parse('$baseUrl$path'), - headers: _headers(token: token), - body: body == null ? null : jsonEncode(body), - ); - return _decodeOrNull(_guardResponse(response)); - } - } - - final parsedBody = _decodeOrNull(response); - final serverMessage = _extractMessage(parsedBody); - - if (response.statusCode == 401) { - throw ApiException( - type: ApiErrorType.unauthorized, - statusCode: 401, - message: serverMessage ?? 'Unauthorized', - ); - } - - if (response.statusCode == 403) { - throw ApiException( - type: ApiErrorType.forbidden, - statusCode: 403, - message: serverMessage ?? 'Forbidden', - ); - } - - if (response.statusCode >= 500) { - throw ApiException( - type: ApiErrorType.server, - statusCode: response.statusCode, - message: serverMessage ?? 'Server error', - ); - } - - throw ApiException( - type: ApiErrorType.unknown, - statusCode: response.statusCode, - message: serverMessage ?? 'Request failed', - ); - } - - dynamic _decodeOrNull(http.Response response) { - final body = response.body.trim(); - if (body.isEmpty) { - return null; - } - - try { - return jsonDecode(body); - } catch (_) { - return body; - } - } - - String? _extractMessage(dynamic parsedBody) { - if (parsedBody is Map) { - final message = parsedBody['message']; - if (message is String && message.trim().isNotEmpty) { - return message; - } - if (message is List && message.isNotEmpty) { - return message.first.toString(); - } - final error = parsedBody['error']; - if (error is String && error.trim().isNotEmpty) { - return error; - } - } - if (parsedBody is String && parsedBody.trim().isNotEmpty) { - return parsedBody; - } - return null; - } +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import 'api_exception.dart'; + +/// Platform-neutral HTTP client. +/// API base URL is injected at build time via --dart-define=API_BASE_URL. +/// Default is same-origin '/api' to avoid mixed-content on HTTPS sites. +class ApiClient { + final String baseUrl; + final http.Client _client; + + ApiClient({http.Client? client}) + : baseUrl = const String.fromEnvironment( + 'API_BASE_URL', + defaultValue: '/api', + ), + _client = client ?? http.Client(); + + Map _headers({String? token}) => { + 'Content-Type': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + + Future getJson(String path, {String? token}) async { + final response = await _client.get( + Uri.parse('$baseUrl$path'), + headers: _headers(token: token), + ); + return _decodeOrNull(_guardResponse(response)); + } + + Future postJson(String path, {Object? body, String? token}) async { + final response = await _client.post( + Uri.parse('$baseUrl$path'), + headers: _headers(token: token), + body: body == null ? null : jsonEncode(body), + ); + return _decodeOrNull(_guardResponse(response)); + } + + Future putJson(String path, {Object? body, String? token}) async { + final response = await _client.put( + Uri.parse('$baseUrl$path'), + headers: _headers(token: token), + body: body == null ? null : jsonEncode(body), + ); + return _decodeOrNull(_guardResponse(response)); + } + + Future patchJson(String path, {Object? body, String? token}) async { + final response = await _client.patch( + Uri.parse('$baseUrl$path'), + headers: _headers(token: token), + body: body == null ? null : jsonEncode(body), + ); + return _decodeOrNull(_guardResponse(response)); + } + + Future deleteJson(String path, {String? token}) async { + final response = await _client.delete( + Uri.parse('$baseUrl$path'), + headers: _headers(token: token), + ); + return _decodeOrNull(_guardResponse(response)); + } + + http.Response _guardResponse(http.Response response) { + if (response.statusCode >= 200 && response.statusCode < 300) { + return response; + } + + final parsedBody = _decodeOrNull(response); + final serverMessage = _extractMessage(parsedBody); + + if (response.statusCode == 401) { + throw ApiException( + type: ApiErrorType.unauthorized, + statusCode: 401, + message: serverMessage ?? 'Unauthorized', + ); + } + + if (response.statusCode == 403) { + throw ApiException( + type: ApiErrorType.forbidden, + statusCode: 403, + message: serverMessage ?? 'Forbidden', + ); + } + + if (response.statusCode >= 500) { + throw ApiException( + type: ApiErrorType.server, + statusCode: response.statusCode, + message: serverMessage ?? 'Server error', + ); + } + + throw ApiException( + type: ApiErrorType.unknown, + statusCode: response.statusCode, + message: serverMessage ?? 'Request failed', + ); + } + + dynamic _decodeOrNull(http.Response response) { + final body = response.body.trim(); + if (body.isEmpty) return null; + try { + return jsonDecode(body); + } catch (_) { + return body; + } + } + + String? _extractMessage(dynamic parsedBody) { + if (parsedBody is Map) { + final message = parsedBody['message']; + if (message is String && message.trim().isNotEmpty) return message; + if (message is List && message.isNotEmpty) { + return message.first.toString(); + } + final error = parsedBody['error']; + if (error is String && error.trim().isNotEmpty) return error; + } + if (parsedBody is String && parsedBody.trim().isNotEmpty) return parsedBody; + return null; + } }