refactor: Clean up ApiClient code structure and improve readability

This commit is contained in:
Nils-Johan Gynther
2026-04-22 08:14:32 +02:00
parent 967121113e
commit 75d993f83a
+130 -139
View File
@@ -1,140 +1,131 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'api_exception.dart'; import 'api_exception.dart';
/// Platform-neutral HTTP client. /// Platform-neutral HTTP client.
/// API base URL is injected at build time via --dart-define=API_BASE_URL. /// 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. /// Default is same-origin '/api' to avoid mixed-content on HTTPS sites.
class ApiClient { class ApiClient {
final String baseUrl; final String baseUrl;
final http.Client _client; final http.Client _client;
ApiClient({http.Client? client}) ApiClient({http.Client? client})
: baseUrl = const String.fromEnvironment( : baseUrl = const String.fromEnvironment(
'API_BASE_URL', 'API_BASE_URL',
defaultValue: '/api', defaultValue: '/api',
), ),
_client = client ?? http.Client(); _client = client ?? http.Client();
Map<String, String> _headers({String? token}) => { Map<String, String> _headers({String? token}) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token', if (token != null) 'Authorization': 'Bearer $token',
}; };
Future<dynamic> getJson(String path, {String? token}) async { Future<dynamic> getJson(String path, {String? token}) async {
final response = await _client.get( final response = await _client.get(
Uri.parse('$baseUrl$path'), Uri.parse('$baseUrl$path'),
headers: _headers(token: token), headers: _headers(token: token),
); );
return _decodeOrNull(_guardResponse(response)); return _decodeOrNull(_guardResponse(response));
} }
Future<dynamic> postJson( Future<dynamic> postJson(String path, {Object? body, String? token}) async {
String path, { final response = await _client.post(
Object? body, Uri.parse('$baseUrl$path'),
String? token, headers: _headers(token: token),
}) async { body: body == null ? null : jsonEncode(body),
final response = await _client.post( );
Uri.parse('$baseUrl$path'), return _decodeOrNull(_guardResponse(response));
headers: _headers(token: token), }
body: body == null ? null : jsonEncode(body),
); Future<dynamic> putJson(String path, {Object? body, String? token}) async {
return _decodeOrNull(_guardResponse(response)); final response = await _client.put(
} Uri.parse('$baseUrl$path'),
headers: _headers(token: token),
Future<dynamic> putJson( body: body == null ? null : jsonEncode(body),
String path, { );
Object? body, return _decodeOrNull(_guardResponse(response));
String? token, }
}) async {
final response = await _client.put( Future<dynamic> patchJson(String path, {Object? body, String? token}) async {
Uri.parse('$baseUrl$path'), final response = await _client.patch(
headers: _headers(token: token), Uri.parse('$baseUrl$path'),
body: body == null ? null : jsonEncode(body), headers: _headers(token: token),
); body: body == null ? null : jsonEncode(body),
return _decodeOrNull(_guardResponse(response)); );
} return _decodeOrNull(_guardResponse(response));
}
Future<dynamic> patchJson(
String path, { Future<dynamic> deleteJson(String path, {String? token}) async {
Object? body, final response = await _client.delete(
String? token, Uri.parse('$baseUrl$path'),
}) async { headers: _headers(token: token),
final response = await _client.patch( );
Uri.parse('$baseUrl$path'), return _decodeOrNull(_guardResponse(response));
headers: _headers(token: token), }
body: body == null ? null : jsonEncode(body),
); http.Response _guardResponse(http.Response response) {
return _decodeOrNull(_guardResponse(response)); if (response.statusCode >= 200 && response.statusCode < 300) {
} return response;
} }
final parsedBody = _decodeOrNull(response); final parsedBody = _decodeOrNull(response);
final serverMessage = _extractMessage(parsedBody); final serverMessage = _extractMessage(parsedBody);
if (response.statusCode == 401) { if (response.statusCode == 401) {
throw ApiException( throw ApiException(
type: ApiErrorType.unauthorized, type: ApiErrorType.unauthorized,
statusCode: 401, statusCode: 401,
message: serverMessage ?? 'Unauthorized', message: serverMessage ?? 'Unauthorized',
); );
} }
if (response.statusCode == 403) { if (response.statusCode == 403) {
throw ApiException( throw ApiException(
type: ApiErrorType.forbidden, type: ApiErrorType.forbidden,
statusCode: 403, statusCode: 403,
message: serverMessage ?? 'Forbidden', message: serverMessage ?? 'Forbidden',
); );
} }
if (response.statusCode >= 500) { if (response.statusCode >= 500) {
throw ApiException( throw ApiException(
type: ApiErrorType.server, type: ApiErrorType.server,
statusCode: response.statusCode, statusCode: response.statusCode,
message: serverMessage ?? 'Server error', message: serverMessage ?? 'Server error',
); );
} }
throw ApiException( throw ApiException(
type: ApiErrorType.unknown, type: ApiErrorType.unknown,
statusCode: response.statusCode, statusCode: response.statusCode,
message: serverMessage ?? 'Request failed', message: serverMessage ?? 'Request failed',
); );
} }
dynamic _decodeOrNull(http.Response response) { dynamic _decodeOrNull(http.Response response) {
final body = response.body.trim(); final body = response.body.trim();
if (body.isEmpty) { if (body.isEmpty) return null;
return null; try {
} return jsonDecode(body);
} catch (_) {
try { return body;
return jsonDecode(body); }
} catch (_) { }
return body;
} String? _extractMessage(dynamic parsedBody) {
} if (parsedBody is Map<String, dynamic>) {
final message = parsedBody['message'];
String? _extractMessage(dynamic parsedBody) { if (message is String && message.trim().isNotEmpty) return message;
if (parsedBody is Map<String, dynamic>) { if (message is List && message.isNotEmpty) {
final message = parsedBody['message']; return message.first.toString();
if (message is String && message.trim().isNotEmpty) { }
return message; final error = parsedBody['error'];
} if (error is String && error.trim().isNotEmpty) return error;
if (message is List && message.isNotEmpty) { }
return message.first.toString(); if (parsedBody is String && parsedBody.trim().isNotEmpty) return parsedBody;
} return null;
final error = parsedBody['error']; }
if (error is String && error.trim().isNotEmpty) {
return error;
}
}
if (parsedBody is String && parsedBody.trim().isNotEmpty) {
return parsedBody;
}
return null;
}
} }