Networking & REST API Integration

Making GET Requests & Parsing JSON Responses

16 min Lesson 2 of 13

Making GET Requests & Parsing JSON Responses

Almost every real-world Flutter application needs to communicate with a remote server. The most common operation is a GET request — asking the server for data — followed by parsing that data from JSON into Dart objects. In this lesson you will learn the complete workflow: add the http package, fire a GET request, inspect the response, and convert the raw JSON string into usable Dart maps and lists using dart:convert.

The http Package

Flutter does not ship with a built-in HTTP client (the dart:io HttpClient exists, but it is verbose). The community-standard solution is the http package published by the Dart team. Add it to pubspec.yaml:

pubspec.yaml dependency

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.1          # Always check pub.dev for the latest version

Run flutter pub get to download it. Then import both libraries at the top of the Dart file that will perform the request:

Required imports

import 'package:http/http.dart' as http;
import 'dart:convert';
Note: Importing http with the as http alias is idiomatic. It prevents name collisions and makes it immediately clear that http.get() comes from the package, not your own code.

Performing a GET Request

http.get() is an async function that returns a Future<http.Response>. You must await it inside an async function. The Uri.parse() factory converts a plain URL string into the Uri type the package expects.

Basic GET request

import 'package:http/http.dart' as http;
import 'dart:convert';

Future<void> fetchUsers() async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/users');

  final response = await http.get(uri);

  if (response.statusCode == 200) {
    // Success — the body is a JSON string
    print('Response body: ${response.body}');
  } else {
    // Non-2xx means something went wrong
    throw Exception('Failed to load users: ${response.statusCode}');
  }
}

The http.Response object exposes several useful members:

  • response.statusCode — the HTTP status code (200, 404, 500, etc.)
  • response.body — the response body as a String
  • response.headers — a Map<String, String> of response headers
  • response.bodyBytes — the raw body as Uint8List (useful for binary data)

Parsing JSON with dart:convert

The dart:convert library provides the jsonDecode() top-level function. It accepts a JSON string and returns a Dart dynamic value — either a Map<String, dynamic> (for a JSON object) or a List<dynamic> (for a JSON array). You then cast and access the fields by name.

Parsing a JSON array of objects

Future<List<Map<String, dynamic>>> fetchPosts() async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final response = await http.get(uri);

  if (response.statusCode != 200) {
    throw Exception('HTTP ${response.statusCode}');
  }

  // jsonDecode returns dynamic — cast it to the expected shape
  final List<dynamic> jsonList = jsonDecode(response.body) as List<dynamic>;

  // Convert each element to a typed Map
  final posts = jsonList
      .map((item) => item as Map<String, dynamic>)
      .toList();

  for (final post in posts) {
    print('Title: ${post['title']}');
    print('Body: ${post['body']}');
    print('User ID: ${post['userId']}');
    print('---');
  }

  return posts;
}
Tip: In production code you almost always map these raw maps into proper Dart model classes with a fromJson factory constructor. That step is covered in the next lesson. For now, working with raw maps lets you understand what jsonDecode actually returns before adding an extra abstraction layer.

Parsing a Single JSON Object

When the server returns a single JSON object (not an array), jsonDecode returns a Map<String, dynamic>. Access fields with the familiar bracket syntax:

Parsing a JSON object

Future<Map<String, dynamic>> fetchUser(int id) async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/users/$id');
  final response = await http.get(uri);

  if (response.statusCode != 200) {
    throw Exception('User not found (${response.statusCode})');
  }

  final Map<String, dynamic> user =
      jsonDecode(response.body) as Map<String, dynamic>;

  // Access individual fields
  final String name    = user['name']  as String;
  final String email   = user['email'] as String;
  final Map<String, dynamic> address =
      user['address'] as Map<String, dynamic>;
  final String city    = address['city'] as String;

  print('Name: $name, Email: $email, City: $city');
  return user;
}
Warning: jsonDecode() returns dynamic. If you access a field that does not exist in the JSON, Dart returns null at runtime — it does NOT throw a compile-time error. Always check for null or use ?.cast<>() when the field may be absent, especially with optional or nullable API fields.

Error Handling & Network Exceptions

Network calls can fail for reasons beyond a bad status code: no internet, DNS failure, server timeout, or a malformed JSON string. Wrap your request in a try-catch to handle both HTTP errors and socket exceptions gracefully:

Robust fetch with error handling

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';          // for SocketException

Future<List<dynamic>> safeFetchPosts() async {
  try {
    final response = await http
        .get(Uri.parse('https://jsonplaceholder.typicode.com/posts'))
        .timeout(const Duration(seconds: 10));

    if (response.statusCode == 200) {
      return jsonDecode(response.body) as List<dynamic>;
    } else {
      throw Exception('Server error: ${response.statusCode}');
    }
  } on SocketException {
    throw Exception('No internet connection');
  } on FormatException {
    throw Exception('Bad response format — could not parse JSON');
  }
}

Summary

To make a GET request and parse JSON in Flutter: add http to your dependencies, import http and dart:convert, call await http.get(Uri.parse(url)), check response.statusCode == 200, and pass response.body to jsonDecode(). The result is either a Map<String, dynamic> or a List<dynamic> that you can iterate or cast. Wrap the call in a try-catch to handle connectivity issues and malformed responses.

Key Takeaway: http.get() + jsonDecode() form the foundation of all Flutter networking. Every higher-level pattern — model classes, repositories, FutureBuilder integration — builds on top of these two primitives.