Reader Stacks

A Simple Way to Call an API in Flutter Using the http Package

The http package's get() and post() methods, combined with dart:convert's jsonDecode, cover most API calls a typical Flutter app needs to make.

A Simple Way to Call an API in Flutter Using the http Package

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.