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
+89 -98
View File
@@ -1,4 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@@ -14,7 +14,7 @@ class ApiClient {
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();
@@ -23,45 +23,33 @@ class ApiClient {
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),
);
return _decodeOrNull(_guardResponse(response));
}
Future<dynamic> putJson( Future<dynamic> putJson(String path, {Object? body, String? token}) async {
String path, { final response = await _client.put(
Object? body, Uri.parse('$baseUrl$path'),
String? token, headers: _headers(token: token),
}) async { body: body == null ? null : jsonEncode(body),
final response = await _client.put( );
Uri.parse('$baseUrl$path'), return _decodeOrNull(_guardResponse(response));
headers: _headers(token: token), }
body: body == null ? null : jsonEncode(body),
);
return _decodeOrNull(_guardResponse(response));
}
Future<dynamic> patchJson( Future<dynamic> patchJson(String path, {Object? body, String? token}) async {
String path, {
Object? body,
String? token,
}) async {
final response = await _client.patch( final response = await _client.patch(
Uri.parse('$baseUrl$path'), Uri.parse('$baseUrl$path'),
headers: _headers(token: token), headers: _headers(token: token),
@@ -69,72 +57,75 @@ class ApiClient {
); );
return _decodeOrNull(_guardResponse(response)); return _decodeOrNull(_guardResponse(response));
} }
}
final parsedBody = _decodeOrNull(response); Future<dynamic> deleteJson(String path, {String? token}) async {
final serverMessage = _extractMessage(parsedBody); final response = await _client.delete(
Uri.parse('$baseUrl$path'),
headers: _headers(token: token),
);
return _decodeOrNull(_guardResponse(response));
}
if (response.statusCode == 401) { http.Response _guardResponse(http.Response response) {
throw ApiException( if (response.statusCode >= 200 && response.statusCode < 300) {
type: ApiErrorType.unauthorized, return response;
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 parsedBody = _decodeOrNull(response);
final body = response.body.trim(); final serverMessage = _extractMessage(parsedBody);
if (body.isEmpty) {
return null;
}
try { if (response.statusCode == 401) {
return jsonDecode(body); throw ApiException(
} catch (_) { type: ApiErrorType.unauthorized,
return body; statusCode: 401,
} message: serverMessage ?? 'Unauthorized',
);
} }
String? _extractMessage(dynamic parsedBody) { if (response.statusCode == 403) {
if (parsedBody is Map<String, dynamic>) { throw ApiException(
final message = parsedBody['message']; type: ApiErrorType.forbidden,
if (message is String && message.trim().isNotEmpty) { statusCode: 403,
return message; message: serverMessage ?? 'Forbidden',
} );
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;
} }
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<String, dynamic>) {
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;
}
} }