Dart 46 🎯 HTTP Client Requests (dart:io and package:http): REST Integration
A Dart application that talks to a REST API has two options. The first is the dart:io library, which provides the HttpClient class and works on the Dart VM, the server, the command-line tools, and the mobile applications. The HttpClient is the low-level, the full-featured client: it handles the persistent connections, the cookies, the authentication, the redirects, and the SSL. The second is the package:http library, which provides the Client abstraction and works on every platform, including the web, where the dart:io is not available. The package:http is the high-level, the cross-platform client: it wraps the platform-specific implementations behind the single API, and it is the recommended choice for the applications that must run everywhere.
The two libraries share the same mental model. A request is a method, a URL, the headers, and the optional body. A response is the status code, the headers, and the body. The dart:io splits the request into the HttpClientRequest and the response into the HttpClientResponse, and the developer reads the response body from the stream. The package:http wraps the request in the Request object and the response in the Response object, and the developer reads the response body from the body property. The package:http is the simpler, and the dart:io is the more controllable.
The REST integration has the recurring patterns. The GET retrieves the resource, the POST creates it, the PUT replaces it, the PATCH updates it partially, and the DELETE removes it. The headers carry the authentication, the content type, and the custom metadata. The JSON is the wire format, and the dart:convert library provides the jsonEncode and the jsonDecode. The error handling covers the network failures, the timeouts, the non-2xx status codes, and the malformed JSON. The retry and the backoff handle the transient failures, and the cancellation handles the abandoned requests.
This chapter covers three areas. First, why the two clients exist — the platform-versus-the cross-platform problem and the low-versus-high-level trade-off. Second, how to make the requests — the GET, the POST, the PUT, the PATCH, and the DELETE, with the headers, the body, and the JSON. Third, the patterns and the error handling — the timeouts, the retries, the cancellation, the authentication, and the typed models. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the request lifecycle.
Key point: The dart:io HttpClient is the low-level client for the Dart VM, the server, the mobile, and the command-line. The package:http Client is the high-level, cross-platform client that works on every platform including the web. The package:http is the recommended choice for the portable applications. The REST requests use the get, the post, the put, the patch, and the delete methods, and the JSON is the wire format. The error handling covers the network failures, the timeouts, the non-2xx statuses, and the malformed JSON.
Why the two clients exist
The platform problem. The dart:io library is not available on the web, because the browser does not expose the sockets and the file system. The web applications use the XMLHttpRequest or the fetch API, and the package:http wraps them behind the same Client interface. The dart:io is for the VM and the mobile; the package:http is for every platform. The choice depends on the target .
The low-level problem. The dart:io HttpClient gives the developer the full control: the persistent connections, the connection pooling, the cookies, the authentication, the redirects, the timeouts, and the SSL certificates. The control is the power and the complexity. The package:http abstracts the details behind the simple API, and the developer loses the fine control but gains the portability and the simplicity .
The streaming problem. The dart:io HttpClientResponse is a stream, and the developer reads the body incrementally. The package:http Response reads the body into the memory, and the developer reads the complete body at once. The streaming is the right choice for the large responses, and the buffered is the right choice for the small ones .
The testing problem. The package:http Client is the abstract class, and the tests can substitute the MockClient for the real client. The dart:io HttpClient is the concrete class, and the tests require the real server or the more complex mocking. The package:http is the testable, and it is the recommended choice for the applications with the unit tests .
The package:http variants. The package:http has the two variants: the Client() constructor that uses the platform-specific implementation, and the Client() from the http/io_client.dart or the http/browser_client.dart for the explicit control. The package:http also has the RetryClient for the automatic retries, and the IOClient and the BrowserClient for the specific platforms .
The trade-off. The dart:io is the more powerful and the more complex. The package:http is the simpler and the more portable. The trade-off is between the control and the portability, and the choice depends on the target platform and the requirements.
a. The GET request
The GET request retrieves the resource. The package:http version is the simple:
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> fetchUsers() async {
final response = await http.get(
Uri.parse('https://api.example.com/users'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
for (final user in data) {
print(user['name']);
}
} else {
throw Exception('Failed to load users: ${response.statusCode}');
}
}
The http.get returns the Response with the statusCode, the headers, and the body. The jsonDecode parses the body into the Dart objects. The status code is the check, and the non-200 is the error .
The headers. The headers are passed as the named argument:
final response = await http.get(
Uri.parse('https://api.example.com/users'),
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
);
The Authorization header carries the bearer token, and the Accept header requests the JSON.
The query parameters. The query parameters are the part of the URL:
final uri = Uri.parse('https://api.example.com/users').replace(
queryParameters: {
'page': '1',
'limit': '20',
'search': 'alice',
},
);
final response = await http.get(uri);
The Uri.replace builds the URL with the encoded query parameters. The Uri handles the encoding, and the developer does not concatenate the strings .
The dart:io version. The dart:io version uses the HttpClient:
import 'dart:io';
import 'dart:convert';
Future<void> fetchUsers() async {
final client = HttpClient();
try {
final request = await client.getUrl(
Uri.parse('https://api.example.com/users'),
);
request.headers.set('Authorization', 'Bearer $token');
final response = await request.close();
if (response.statusCode == 200) {
final body = await response.transform(utf8.decoder).join();
final List<dynamic> data = jsonDecode(body);
for (final user in data) {
print(user['name']);
}
} else {
throw Exception('Failed to load users: ${response.statusCode}');
}
} finally {
client.close();
}
}
The client.getUrl opens the request, the request.close() sends it and returns the response. The response.transform(utf8.decoder).join() reads the body as the string. The client.close() releases the resources, and the finally ensures it runs .
b. The POST, the PUT, and the PATCH
The POST creates the resource, the PUT replaces it, and the PATCH updates it partially. The three send the body, and the content type is the application/json.
The POST.
Future<User> createUser(String name, String email) async {
final response = await http.post(
Uri.parse('https://api.example.com/users'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode({
'name': name,
'email': email,
}),
);
if (response.statusCode == 201) {
return User.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create user: ${response.statusCode}');
}
}
The jsonEncode serializes the map into the JSON string, and the Content-Type header tells the server the format. The 201 is the created status, and the User.fromJson parses the response .
The PUT.
Future<User> replaceUser(int id, User user) async {
final response = await http.put(
Uri.parse('https://api.example.com/users/$id'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode(user.toJson()),
);
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to replace user: ${response.statusCode}');
}
}
The PUT sends the complete representation, and the server replaces the resource.
The PATCH.
Future<User> updateUser(int id, Map<String, dynamic> changes) async {
final response = await http.patch(
Uri.parse('https://api.example.com/users/$id'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode(changes),
);
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to update user: ${response.statusCode}');
}
}
The PATCH sends only the changed fields, and the server merges them with the existing resource .
The DELETE.
Future<void> deleteUser(int id) async {
final response = await http.delete(
Uri.parse('https://api.example.com/users/$id'),
headers: {
'Authorization': 'Bearer $token',
},
);
if (response.statusCode != 204 && response.statusCode != 200) {
throw Exception('Failed to delete user: ${response.statusCode}');
}
}
The DELETE removes the resource, and the 204 is the no-content success. The 200 is the success with the body .
c. The patterns and the error handling
The typed client. The typed client wraps the http.Client and provides the domain-specific methods:
class ApiClient {
final http.Client _client;
final String _baseUrl;
final String? _token;
ApiClient({http.Client? client, required String baseUrl, String? token})
: _client = client ?? http.Client(),
_baseUrl = baseUrl,
_token = token;
Map<String, String> get _headers => {
'Content-Type': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};
Future<List<User>> getUsers() async {
final response = await _client.get(
Uri.parse('$_baseUrl/users'),
headers: _headers,
);
_checkStatus(response, 200);
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => User.fromJson(json)).toList();
}
Future<User> createUser(User user) async {
final response = await _client.post(
Uri.parse('$_baseUrl/users'),
headers: _headers,
body: jsonEncode(user.toJson()),
);
_checkStatus(response, 201);
return User.fromJson(jsonDecode(response.body));
}
void _checkStatus(http.Response response, int expected) {
if (response.statusCode != expected) {
throw ApiException(
statusCode: response.statusCode,
message: response.body,
);
}
}
void close() => _client.close();
}
The typed client is the single place for the base URL, the headers, the error handling, and the JSON parsing. The rest of the application uses the domain methods, and the HTTP details are the implementation .
The custom exception.
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException({required this.statusCode, required this.message});
@override
String toString() => 'ApiException($statusCode): $message';
}
The custom exception carries the status code and the message, and the caller can catch the specific type.
The timeout.
Future<List<User>> getUsers() async {
try {
final response = await _client
.get(Uri.parse('$_baseUrl/users'), headers: _headers)
.timeout(const Duration(seconds: 10));
_checkStatus(response, 200);
return (jsonDecode(response.body) as List)
.map((json) => User.fromJson(json))
.toList();
} on TimeoutException {
throw ApiException(statusCode: 408, message: 'Request timed out');
} on SocketException {
throw ApiException(statusCode: 0, message: 'Network error');
} on FormatException {
throw ApiException(statusCode: 500, message: 'Invalid response format');
}
}
The .timeout throws the TimeoutException when the request exceeds the duration. The SocketException is the network failure, and the FormatException is the malformed JSON .
The retry. The RetryClient from the package:http/retry.dart provides the automatic retries:
import 'package:http/retry.dart';
final client = RetryClient(
http.Client(),
retries: 3,
when: (response) => response.statusCode == 503,
delay: (retryCount) => Duration(seconds: retryCount * 2),
);
The RetryClient retries the request when the when predicate returns true, with the delay based on the delay function. The exponential backoff is the common pattern .
The cancellation. The package:http does not have the built-in cancellation, and the pattern is the AbortableRequest or the manual cancellation with the Client.close(). The package:http v1.2 introduced the AbortableRequest:
final request = http.AbortableRequest('GET', uri);
final response = await client.send(request);
// Later:
request.abort();
The abort() cancels the in-flight request, and the send throws the RequestAbortedException .
The authentication. The bearer token is the header, and the Basic authentication is the base64-encoded username and password:
final credentials = base64Encode(utf8.encode('$username:$password'));
final response = await http.get(
uri,
headers: {'Authorization': 'Basic $credentials'},
);
The base64Encode from the dart:convert encodes the credentials, and the Authorization header carries them .
Complete Example Session
// ============================================
// PART 1: THE BASIC GET
// ============================================
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> fetchUsers() async {
final response = await http.get(
Uri.parse('https://api.example.com/users'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
for (final user in data) {
print(user['name']);
}
} else {
throw Exception('Failed to load users: ${response.statusCode}');
}
}
// ============================================
// PART 2: THE GET WITH THE HEADERS AND THE QUERY
// ============================================
Future<List<User>> searchUsers(String query) async {
final uri = Uri.parse('https://api.example.com/users').replace(
queryParameters: {'search': query, 'limit': '20'},
);
final response = await http.get(
uri,
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => User.fromJson(json)).toList();
}
throw Exception('Search failed: ${response.statusCode}');
}
// ============================================
// PART 3: THE POST
// ============================================
Future<User> createUser(String name, String email) async {
final response = await http.post(
Uri.parse('https://api.example.com/users'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode({'name': name, 'email': email}),
);
if (response.statusCode == 201) {
return User.fromJson(jsonDecode(response.body));
}
throw Exception('Failed to create user: ${response.statusCode}');
}
// ============================================
// PART 4: THE PUT AND THE PATCH
// ============================================
Future<User> replaceUser(int id, User user) async {
final response = await http.put(
Uri.parse('https://api.example.com/users/$id'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(user.toJson()),
);
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
}
throw Exception('Failed to replace user: ${response.statusCode}');
}
Future<User> patchUser(int id, Map<String, dynamic> changes) async {
final response = await http.patch(
Uri.parse('https://api.example.com/users/$id'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(changes),
);
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
}
throw Exception('Failed to update user: ${response.statusCode}');
}
// ============================================
// PART 5: THE DELETE
// ============================================
Future<void> deleteUser(int id) async {
final response = await http.delete(
Uri.parse('https://api.example.com/users/$id'),
);
if (response.statusCode != 204 && response.statusCode != 200) {
throw Exception('Failed to delete user: ${response.statusCode}');
}
}
// ============================================
// PART 6: THE TYPED CLIENT
// ============================================
class ApiClient {
final http.Client _client;
final String _baseUrl;
final String? _token;
ApiClient({http.Client? client, required String baseUrl, String? token})
: _client = client ?? http.Client(),
_baseUrl = baseUrl,
_token = token;
Map<String, String> get _headers => {
'Content-Type': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};
Future<List<User>> getUsers() async {
final response = await _client.get(
Uri.parse('$_baseUrl/users'),
headers: _headers,
);
_checkStatus(response, 200);
return (jsonDecode(response.body) as List)
.map((json) => User.fromJson(json))
.toList();
}
Future<User> createUser(User user) async {
final response = await _client.post(
Uri.parse('$_baseUrl/users'),
headers: _headers,
body: jsonEncode(user.toJson()),
);
_checkStatus(response, 201);
return User.fromJson(jsonDecode(response.body));
}
void _checkStatus(http.Response response, int expected) {
if (response.statusCode != expected) {
throw ApiException(
statusCode: response.statusCode,
message: response.body,
);
}
}
void close() => _client.close();
}
// ============================================
// PART 7: THE CUSTOM EXCEPTION
// ============================================
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException({required this.statusCode, required this.message});
@override
String toString() => 'ApiException($statusCode): $message';
}
// ============================================
// PART 8: THE TIMEOUT AND THE ERROR HANDLING
// ============================================
Future<List<User>> getUsersWithTimeout() async {
try {
final response = await http
.get(Uri.parse('https://api.example.com/users'))
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
return (jsonDecode(response.body) as List)
.map((json) => User.fromJson(json))
.toList();
}
throw ApiException(
statusCode: response.statusCode,
message: response.body,
);
} on TimeoutException {
throw ApiException(statusCode: 408, message: 'Request timed out');
} on SocketException {
throw ApiException(statusCode: 0, message: 'Network error');
} on FormatException {
throw ApiException(statusCode: 500, message: 'Invalid response format');
}
}
// ============================================
// PART 9: THE RETRY CLIENT
// ============================================
import 'package:http/retry.dart';
final retryClient = RetryClient(
http.Client(),
retries: 3,
when: (response) => response.statusCode == 503,
delay: (retryCount) => Duration(seconds: retryCount * 2),
);
Future<void> fetchWithRetry() async {
final response = await retryClient.get(
Uri.parse('https://api.example.com/data'),
);
print(response.body);
}
// ============================================
// PART 10: THE COMPLETE INTEGRATION
// ============================================
class UserRepository {
final ApiClient _api;
UserRepository(this._api);
Future<List<User>> getAll() => _api.getUsers();
Future<User> create(String name, String email) async {
try {
return await _api.createUser(User(name: name, email: email));
} on ApiException catch (e) {
if (e.statusCode == 409) {
throw ApiException(
statusCode: 409,
message: 'A user with this email already exists',
);
}
rethrow;
}
}
}
void main() async {
final api = ApiClient(
baseUrl: 'https://api.example.com',
token: 'your-token',
);
final repo = UserRepository(api);
try {
final users = await repo.getAll();
print('Users: ${users.length}');
final newUser = await repo.create('Alice', 'alice@example.com');
print('Created: ${newUser.name}');
} on ApiException catch (e) {
print('API error: $e');
} finally {
api.close();
}
}
The ten parts show the basic GET, the GET with the headers and the query, the POST, the PUT and the PATCH, the DELETE, the typed client, the custom exception, the timeout and the error handling, the retry client, and the complete integration.
Quick Reference
The package:http Methods
| Method | Purpose |
|---|---|
http.get(uri, {headers}) | The GET request |
http.post(uri, {headers, body}) | The POST request |
http.put(uri, {headers, body}) | The PUT request |
http.patch(uri, {headers, body}) | The PATCH request |
http.delete(uri, {headers}) | The DELETE request |
http.head(uri, {headers}) | The HEAD request |
The Request and the Response
| The property | The meaning |
|---|---|
response.statusCode | The HTTP status |
response.headers | The response headers |
response.body | The body as the string |
response.bodyBytes | The body as the bytes |
response.isRedirect | The redirect flag |
The Content Types
| The header | The value |
|---|---|
Content-Type | application/json |
Accept | application/json |
Authorization | Bearer <token> |
Authorization | Basic <base64> |
The Error Handling
| The exception | The cause |
|---|---|
TimeoutException | The request exceeded the duration |
SocketException | The network failure |
FormatException | The malformed JSON |
ApiException | The non-2xx status |
ClientException | The general client error |
The Status Codes
| The code | The meaning |
|---|---|
| 200 | The OK |
| 201 | The Created |
| 204 | The No Content |
| 400 | The Bad Request |
| 401 | The Unauthorized |
| 403 | The Forbidden |
| 404 | The Not Found |
| 409 | The Conflict |
| 500 | The Internal Server Error |
| 503 | The Service Unavailable |
The Retry and the Timeout
| The tool | The purpose |
|---|---|
.timeout(Duration) | The request timeout |
RetryClient | The automatic retries |
AbortableRequest | The cancellation |
Client.close() | The resource release |
Best Practices
✅ Do This:
// Use the package:http for the cross-platform
import 'package:http/http.dart' as http; // ✅
// Use the Uri.parse for the URLs
final uri = Uri.parse('https://api.example.com/users'); // ✅
// Use the jsonEncode for the body
body: jsonEncode({'name': name}), // ✅
// Use the typed client for the encapsulation
class ApiClient { ... } // ✅
// Use the timeout for the requests
.timeout(const Duration(seconds: 10)); // ✅
// Use the RetryClient for the transient failures
final client = RetryClient(http.Client(), retries: 3); // ✅
// Use the custom exception for the errors
throw ApiException(statusCode: 409, message: '...'); // ✅
// Close the client when done
api.close(); // ✅
❌ Don’t Do This:
// Don't concatenate the query parameters
'https://api.example.com/users?search=' + query; // the encoding // ❌
// Don't forget the Content-Type for the JSON
http.post(uri, body: jsonEncode(data)); // the missing header // ❌
// Don't ignore the status code
final response = await http.get(uri); print(response.body); // no check // ❌
// Don't create the client per request
http.get(uri); // the global function; use the Client for the reuse // ❌
// Don't forget the timeout
await http.get(uri); // the infinite wait // ❌
// Don't use the dart:io on the web
import 'dart:io'; // the web is not supported // ❌
// Don't forget the close
final client = http.Client(); // never closed // ❌
Common Pitfalls
| The pitfall | The why | The fix |
|---|---|---|
| The connection leak | The client not closed | Use the finally or the close() |
| The timeout | No .timeout | Add the timeout |
The SocketException | The network failure | Catch and retry |
| The malformed JSON | The wrong body | Catch the FormatException |
| The 401 | The expired token | Refresh the token |
| The portability | The dart:io on the web | Use the package:http |
| The query encoding | The string concatenation | Use the Uri.replace |
Real-World Examples
1. The Basic GET
final response = await http.get(Uri.parse('https://api.example.com/users'));
2. The GET with the Headers
http.get(uri, headers: {'Authorization': 'Bearer $token'});
3. The POST
http.post(uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode(data));
4. The PUT
http.put(uri, headers: headers, body: jsonEncode(user.toJson()));
5. The PATCH
http.patch(uri, headers: headers, body: jsonEncode(changes));
6. The DELETE
http.delete(uri, headers: headers);
7. The Typed Client
class ApiClient { Future<List<User>> getUsers() async { ... } }
8. The Timeout
await http.get(uri).timeout(const Duration(seconds: 10));
9. The Retry
final client = RetryClient(http.Client(), retries: 3);
10. The Complete
final api = ApiClient(baseUrl: 'https://api.example.com');
try { await api.getUsers(); } finally { api.close(); }
Visual
The Request Lifecycle
┌──────────────────────────────────────────────────────────────┐
│ THE REQUEST LIFECYCLE │
│ │
│ The application: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ final response = await http.get(uri); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The package:http: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Creates the Request object. │ │
│ │ Sets the method, the URL, the headers. │ │
│ │ Calls the platform-specific send. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The platform (the dart:io or the browser): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Opens the socket or the fetch. │ │
│ │ Sends the request bytes. │ │
│ │ Reads the response bytes. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The Response: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The statusCode, the headers, the body. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The application: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Checks the status, decodes the JSON. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The dart:io vs the package:http
┌──────────────────────────────────────────────────────────────┐
│ THE dart:io vs THE package:http │
│ │
│ THE dart:io HttpClient: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The VM, the server, the mobile, the CLI. │ │
│ │ The streaming response. │ │
│ │ The fine control: the cookies, the redirects. │ │
│ │ The connection pooling. │ │
│ │ NOT the web. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ THE package:http Client: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Every platform: the VM, the web, the mobile. │ │
│ │ The buffered response. │ │
│ │ The simple API: the get, the post, the put. │ │
│ │ The MockClient for the tests. │ │
│ │ The RetryClient for the retries. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The package:http is the portable, and the dart:io is the │
│ controllable. │
│ │
└──────────────────────────────────────────────────────────────┘
The REST Methods
┌──────────────────────────────────────────────────────────────┐
│ THE REST METHODS │
│ │
│ GET /users → the list of the users │
│ GET /users/1 → the user 1 │
│ POST /users → the create the user │
│ PUT /users/1 → the replace the user 1 │
│ PATCH /users/1 → the update the user 1 partially │
│ DELETE /users/1 → the delete the user 1 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The status codes: │ │
│ │ 200 the OK │ │
│ │ 201 the Created │ │
│ │ 204 the No Content │ │
│ │ 400 the Bad Request │ │
│ │ 401 the Unauthorized │ │
│ │ 404 the Not Found │ │
│ │ 409 the Conflict │ │
│ │ 500 the Internal Server Error │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Error Handling
┌──────────────────────────────────────────────────────────────┐
│ THE ERROR HANDLING │
│ │
│ try { │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ final response = await http.get(uri) │ │
│ │ .timeout(Duration(seconds: 10)); │ │
│ │ │ │
│ │ if (response.statusCode == 200) { │ │
│ │ return User.fromJson(jsonDecode(response.body)); │ │
│ │ } │ │
│ │ throw ApiException(statusCode: response.statusCode);│ │
│ └──────────────────────────────────────────────────────┘ │
│ } on TimeoutException { │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The request exceeded the duration. │ │
│ └──────────────────────────────────────────────────────┘ │
│ } on SocketException { │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The network failure. │ │
│ └──────────────────────────────────────────────────────┘ │
│ } on FormatException { │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The malformed JSON. │ │
│ └──────────────────────────────────────────────────────┘ │
│ } │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
dart:io HttpClient | The low-level, VM-only client |
package:http Client | The high-level, cross-platform client |
http.get | The GET request |
http.post | The POST request |
http.put | The PUT request |
http.patch | The PATCH request |
http.delete | The DELETE request |
jsonEncode / jsonDecode | The JSON serialization |
.timeout | The request timeout |
RetryClient | The automatic retries |
ApiException | The custom exception |
Key takeaways:
- The
dart:ioHttpClientis the low-level client for the VM, the server, the mobile, and the CLI. It provides the streaming, the connection pooling, the cookies, and the fine control. It is not available on the web . - The
package:httpClientis the high-level, cross-platform client. It wraps the platform-specific implementations behind the single API, and it works on every platform including the web. TheMockClientand theRetryClientare the additional tools . - The REST methods are the
get, thepost, theput, thepatch, and thedelete. Thegetretrieves, thepostcreates, theputreplaces, thepatchupdates partially, and thedeleteremoves. The status codes indicate the result . - The JSON is the wire format. The
jsonEncodeserializes the Dart objects into the JSON, and thejsonDecodeparses the JSON into the Dart objects. TheContent-Type: application/jsonheader tells the server the format . - The typed client encapsulates the HTTP details. The base URL, the headers, the error handling, and the JSON parsing live in the single place. The rest of the application uses the domain methods, and the HTTP details are the implementation .
- The timeout, the retry, and the cancellation handle the failures. The
.timeoutprevents the infinite wait, theRetryClientretries the transient failures, and theAbortableRequestcancels the abandoned requests . - The error handling covers the network, the timeout, the status, and the malformed JSON. The
SocketExceptionis the network failure, theTimeoutExceptionis the timeout, theApiExceptionis the non-2xx status, and theFormatExceptionis the malformed JSON . - The
package:httpis the recommended choice for the portable applications. Thedart:iois for the VM-specific, and thepackage:httpis for the cross-platform. The choice depends on the target platform and the requirements .
Remember: The dart:io and the package:http are the two HTTP clients for the Dart. The dart:io is the low-level, the VM-specific, and the controllable. The package:http is the high-level, the cross-platform, and the simple. The REST requests use the get, the post, the put, the patch, and the delete, and the JSON is the wire format. The typed client encapsulates the details, and the error handling covers the network, the timeout, the status, and the malformed JSON. The result is the reliable REST integration, which is the foundation of the modern application.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!