The http package is Flutter/Dart's most commonly used way to call a REST API — it returns a raw response body as a JSON string, leaving decoding and mapping to a typed model as explicit, separate steps.
Adding the package
# pubspec.yaml
dependencies:
http: ^1.2.0
A basic GET request
import 'package:http/http.dart' as http;
import 'dart:convert';
Future> fetchProducts() async {
final response = await http.get(Uri.parse('https://api.example.com/products'));
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((json) => Product.fromJson(json)).toList();
} else {
throw Exception('Failed to load products');
}
}
Checking response.statusCode explicitly is necessary — unlike some HTTP libraries in other languages, http.get() does not automatically throw an exception for a non-2xx response; a 404 or 500 response still returns normally, just with a different status code and body.
Defining the model with a fromJson factory
class Product {
final int id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
factory Product.fromJson(Map json) {
return Product(
id: json['id'],
name: json['name'],
price: (json['price'] as num).toDouble(),
);
}
}
Casting json['price'] through as num before calling .toDouble() handles a common inconsistency where a JSON API returns a whole-number price as an integer rather than a float — without this cast, a price like 20 (rather than 20.0) would throw a type error when the code expects a double.
A POST request with a JSON body
Future createProduct(String name, double price) async {
final response = await http.post(
Uri.parse('https://api.example.com/products'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'name': name, 'price': price}),
);
if (response.statusCode != 201) {
throw Exception('Failed to create product');
}
}
The Content-Type: application/json header must be set explicitly — unlike Angular's HttpClient or similar libraries in other frameworks, the http package doesn't infer or set this automatically based on the body's shape.
Adding an authorization header
final response = await http.get(
Uri.parse('https://api.example.com/orders'),
headers: {'Authorization': 'Bearer $token'},
);
Handling a network error (not just a bad status code)
Future> fetchProducts() async {
try {
final response = await http.get(Uri.parse('https://api.example.com/products'));
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((json) => Product.fromJson(json)).toList();
}
throw Exception('Server error: ${response.statusCode}');
} on SocketException {
throw Exception('No internet connection.');
}
}
A SocketException (no internet connection, DNS failure) is a genuinely separate failure mode from a bad HTTP status code — the request never even reaches the server in this case, which is why it needs its own catch clause distinct from checking response.statusCode.
Setting a request timeout
final response = await http
.get(Uri.parse('https://api.example.com/products'))
.timeout(const Duration(seconds: 10));
Without an explicit timeout, a request to an unresponsive server can hang indefinitely from the app's perspective — .timeout() throws a TimeoutException after the specified duration, giving the app a bounded, predictable failure case to handle instead.