Flutter's http package is the standard, straightforward way to call a REST API from a Flutter app — combined with dart:convert's JSON handling, it covers most of what a typical app-to-API integration needs.
Adding the package
flutter pub add http
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 statusCode explicitly matters — http.get() doesn't throw automatically on a 404 or 500 response the way some other HTTP clients do, only on genuine network-level failures.
A model class 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 through num before calling .toDouble() handles the case where the API returns a price as either a JSON integer or a float — a JSON 19 versus 19.99 would otherwise deserialize to different Dart types and cause a runtime type error.
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');
}
}
Adding an authorization header
final response = await http.get(
Uri.parse('https://api.example.com/orders'),
headers: {'Authorization': 'Bearer $token'},
);
Using FutureBuilder to display the result in a widget
FutureBuilder>(
future: fetchProducts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return ListView(
children: snapshot.data!.map((p) => ListTile(title: Text(p.name))).toList(),
);
}
},
)
FutureBuilder is the idiomatic Flutter way to show a loading spinner while an async call is in flight, an error message if it fails, and the actual result once it resolves — all driven directly from the Future returned by the fetch function above.
When to reach for a heavier HTTP client instead
For an app making many API calls with shared configuration (base URL, common headers, interceptors for auth token refresh), a package like dio offers more built-in structure — for a smaller app or a handful of endpoints, the plain http package shown here is genuinely sufficient and simpler to reason about.