Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
FREEDOM251

Freedom251 › Blog › Dart

Flutter REST API Calls: Fetch and Parse JSON

By Freedom251 Team · · 12 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To connect a REST API in Flutter, add the http package, build a Uri, await an HTTP request, reject unexpected status codes, decode the JSON, map it to a Dart model, and expose loading, success, and error states to the widget tree. The small example below fetches one album from JSONPlaceholder, then extends the same pattern to POST, PUT, DELETE, authentication, testing, and large responses.

What a Flutter REST connection actually does

A REST call is a network conversation between your app and a server. Your Flutter code chooses an HTTP method and URL, optionally sends headers and a body, waits for the server, checks the response, and converts the response payload into values the UI can use.

As an Amazon Associate I earn from qualifying purchases.

The request is asynchronous because the server may be on another network, may be slow, or may fail. A function that performs the call therefore returns a Future. Using await keeps the code readable without blocking Flutter’s main isolate while the network operation is in progress.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For ordinary cross-platform requests, Flutter’s networking documentation describes the http package as the simplest way to issue HTTP requests. It supports Android, iOS, macOS, Windows, Linux, and web, so you do not have to write separate request code around dart:io and dart:html.

1. Add the HTTP dependency

From the directory containing pubspec.yaml, run:

flutter pub add http

Then import the package with an alias. The alias makes it obvious which classes and methods belong to the package:

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

The command selects the current compatible package release for your Flutter project. This guide intentionally does not pin a version; run the command in the project you intend to build and commit the resulting dependency lockfile when appropriate.

2. Check platform access before debugging Dart code

The request code can be correct while the app is blocked by platform configuration. The official Flutter guidance calls out these target-specific settings:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Android

Add the Internet permission to android/app/src/main/AndroidManifest.xml, normally as a child of the <manifest> element and outside <application>:

<uses-permission android:name="android.permission.INTERNET" />

Do not assume this exact edit is required on every target. Android is the platform for which this permission is explicitly called out in the Flutter networking documentation.

macOS

For a macOS app, enable the network client entitlement in the app’s entitlements file:

<key>com.apple.security.network.client</key>
<true/>

Use the entitlements file generated for your macOS target and keep the setting with the target configuration that is actually signed and run. iOS, Windows, Linux, and web have different platform policies; check the platform’s current Flutter and deployment documentation if a request works on one target but not another.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. Start with a typed model

JSON is untyped text until your program decodes it. A Dart model gives the rest of your app a stable, typed interface and provides one place to validate required fields.

The teaching endpoint in the Flutter cookbook is JSONPlaceholder’s album resource: https://jsonplaceholder.typicode.com/albums/1. A typical response has an integer userId, an integer id, and a string title. Define those fields explicitly:

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });

  factory Album.fromJson(Map<String, dynamic> json) {
    final userId = json['userId'];
    final id = json['id'];
    final title = json['title'];

    if (userId is! int || id is! int || title is! String) {
      throw const FormatException('Album response has an unexpected shape');
    }

    return Album(userId: userId, id: id, title: title);
  }
}

The checks are deliberate. If a backend omits a required field, changes a number to a string, or returns an error object where an album was expected, the app should enter its error state rather than display a misleading empty object.

4. Fetch JSON with GET

Keep networking in a function that returns Future<Album>. Parse the URL as a Uri, await the response, check the status code, decode the body, and construct the model:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Future<Album> fetchAlbum({http.Client? client}) async {
  final httpClient = client ?? http.Client();
  final uri = Uri.parse(
    'https://jsonplaceholder.typicode.com/albums/1',
  );

  try {
    final response = await httpClient.get(uri);

    if (response.statusCode != 200) {
      throw HttpException(
        'GET $uri failed with status ${response.statusCode}',
      );
    }

    final decoded = jsonDecode(response.body);
    if (decoded is! Map<String, dynamic>) {
      throw const FormatException('Expected a JSON object');
    }

    return Album.fromJson(decoded);
  } finally {
    if (client == null) {
      httpClient.close();
    }
  }
}

If you use HttpException, import it from dart:io in a mobile or desktop-only layer, or define an application exception that also works for web. The important behavior is the same: a non-success status must become an error rather than being passed to fromJson as if it were valid album data.

The cookbook checks for status 200 for this GET. That number is not universal: follow the API contract for the method you are calling. A server may return another successful status for a different operation, and an API can use a structured error body even when the transport itself completed normally.

5. Keep the future out of build()

Flutter can call build() many times—for example, after a parent rebuilds, a theme changes, or local state updates. Starting the request directly inside build() can therefore issue the same network call repeatedly.

For a one-time load, create and store the future in initState(). Then give that future to FutureBuilder:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
class AlbumPage extends StatefulWidget {
  const AlbumPage({super.key});

  @override
  State<AlbumPage> createState() => _AlbumPageState();
}

class _AlbumPageState extends State<AlbumPage> {
  late Future<Album> _albumFuture;

  @override
  void initState() {
    super.initState();
    _albumFuture = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Album')),
      body: FutureBuilder<Album>(
        future: _albumFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          if (snapshot.hasError) {
            return Center(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Text('Could not load the album: ${snapshot.error}'),
              ),
            );
          }

          final album = snapshot.data;
          if (album == null) {
            return const Center(child: Text('The server returned no album.'));
          }

          return ListTile(
            title: Text(album.title),
            subtitle: Text('Album ${album.id} · user ${album.userId}'),
          );
        },
      ),
    );
  }
}

What each state means

  • Waiting: the future has not completed, so show progress or a skeleton rather than blank content.
  • Error: DNS failure, a timeout, a non-success status, malformed JSON, or model validation failure can all arrive here. Give the user a useful retry path in a production screen.
  • Data: the model has passed decoding and validation, so widgets can safely use its typed fields.

To refresh intentionally, assign a new future in a button handler and call setState:

void _reload() {
  setState(() {
    _albumFuture = fetchAlbum();
  });
}

This explicit refresh is different from accidentally starting a request during every rebuild.

6. A complete small flow, from URL to widget

The sequence to remember is:

  1. Represent the endpoint as a parsed Uri.
  2. Call the method on an asynchronous function.
  3. Await the response without blocking the UI isolate.
  4. Check the status code before decoding a success payload.
  5. Run jsonDecode from dart:convert.
  6. Verify the decoded value has the expected JSON shape.
  7. Construct a typed model and let validation failures become errors.
  8. Expose the future to FutureBuilder or another state-management layer.

JSONPlaceholder is an educational sample API. Its album fields and behavior do not establish the schema, authentication, persistence, pagination, rate limits, or status codes of your real service. Replace the URL and model rules with that service’s documentation.

7. Send data with POST

A write request adds a JSON body and a content type. Encode a Dart map with jsonEncode and check the status expected for creation. The Flutter cookbook’s create example checks 201:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Future<Album> createAlbum(String title) async {
  final response = await http.post(
    Uri.parse('https://example.invalid/albums'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, dynamic>{
      'title': title,
      'userId': 1,
    }),
  );

  if (response.statusCode != 201) {
    throw HttpException(
      'POST failed with status ${response.statusCode}',
    );
  }

  final decoded = jsonDecode(response.body);
  if (decoded is! Map<String, dynamic>) {
    throw const FormatException('Expected a created album object');
  }
  return Album.fromJson(decoded);
}

The example.invalid host is intentionally not a working service. Substitute your API’s documented creation endpoint, payload fields, and expected response. Some APIs return 200 or 202 for a write; do not copy 201 unless the service specifies it.

8. Update and delete resources

PUT for a replacement

Use PUT when the API defines a replacement or idempotent update. Send the fields required by that API and validate its documented success status:

Future<Album> replaceAlbum(int id, String title) async {
  final response = await http.put(
    Uri.parse('https://api.example.invalid/albums/$id'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, dynamic>{
      'id': id,
      'title': title,
      'userId': 1,
    }),
  );

  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw HttpException('PUT failed with status ${response.statusCode}');
  }

  final decoded = jsonDecode(response.body);
  if (decoded is! Map<String, dynamic>) {
    throw const FormatException('Expected an updated album object');
  }
  return Album.fromJson(decoded);
}

DELETE for removal

DELETE responses frequently contain no JSON body. Check the status first and decode only if the API promises a response payload:

Future<void> deleteAlbum(int id) async {
  final response = await http.delete(
    Uri.parse('https://api.example.invalid/albums/$id'),
  );

  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw HttpException('DELETE failed with status ${response.statusCode}');
  }
}

Method semantics and status codes belong to the server contract. A successful DELETE may be represented by different 2xx codes, and a server may return a body that your application should record or display.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

9. Add authentication without guessing the scheme

Protected services normally expect credentials in an Authorization header, but the scheme is chosen by the service. Read its documentation and send exactly the required form. A bearer-token example looks like this:

Future<http.Response> fetchPrivateResource(String token) {
  return http.get(
    Uri.parse('https://api.example.invalid/private-resource'),
    headers: <String, String>{
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
    },
  );
}

The token above is a variable, not a credential. Never commit a real secret to source control or print it in an exception. Some services use API keys, signed requests, cookies, or another scheme; do not substitute bearer authentication unless the API says to use it.

10. Make failures understandable and recoverable

A production request can fail at several layers. Treat each layer as a separate diagnostic problem:

  • Transport failure: no connection, DNS failure, certificate problem, or a device that is offline. Catch the exception and offer retry.
  • HTTP failure: the server responded with 4xx or 5xx. Preserve the status code and, when safe, parse the service’s documented error body.
  • Decode failure: the body is not valid JSON or is not the type you expected.
  • Validation failure: JSON is valid but required fields have the wrong types or are missing.
  • Application failure: the response is valid but cannot be used, such as an expired session or a business-rule rejection.

Use a timeout when an operation should not wait indefinitely:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
final response = await http
    .get(uri)
    .timeout(const Duration(seconds: 15));

Choose the duration for the user action and service behavior rather than treating 15 seconds as a universal value. A timeout should become a recoverable state, not an uncaught exception that leaves a spinner running forever.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

11. Parse large JSON responses without janking the UI

Do not start with isolate complexity for a small object such as one album. Flutter’s background-parsing recipe addresses expensive parsing with an example containing 5,000 photo objects and explains that work taking more than 16 milliseconds can cause visible frame jank. That is an instructional threshold in the recipe, not a guarantee about every device or payload.

When profiling shows decoding is expensive, move the CPU-heavy conversion to another isolate with compute:

import 'dart:convert';
import 'package:flutter/foundation.dart';

List<Album> parseAlbums(String body) {
  final decoded = jsonDecode(body);
  if (decoded is! List<dynamic>) {
    throw const FormatException('Expected a JSON array');
  }

  return decoded.map((item) {
    if (item is! Map<String, dynamic>) {
      throw const FormatException('Expected an album object');
    }
    return Album.fromJson(item);
  }).toList();
}

Future<List<Album>> decodeAlbumsInBackground(String body) {
  return compute(parseAlbums, body);
}

The function passed to compute should be a top-level or otherwise isolate-safe function, and its input and output must be transferable. Measure before adding this complexity; a small response is usually clearer and fast enough on the main isolate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

12. Inject an HTTP client so tests do not use the network

Passing an http.Client into the fetch function separates request logic from the concrete network implementation. The default can create and close its own client, while a test can provide a mock client and deterministic response.

Future<Album> fetchAlbumWithClient(http.Client client) async {
  final response = await client.get(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
  );

  if (response.statusCode != 200) {
    throw HttpException('Unexpected status ${response.statusCode}');
  }

  final decoded = jsonDecode(response.body);
  if (decoded is! Map<String, dynamic>) {
    throw const FormatException('Expected a JSON object');
  }
  return Album.fromJson(decoded);
}

A unit test can then verify success, a non-200 response, malformed JSON, and a missing field without depending on an Internet connection. Flutter’s cookbook uses client injection as the basis for mocking request code. Keep the client’s lifetime clear: a long-lived client can be owned by a repository, while a short-lived client should be closed after use.

13. Decide what belongs in a larger app

The direct function-and-FutureBuilder approach is enough for a small screen. As the app grows, introduce structure only to solve a real problem:

  • Repository layer: centralizes endpoints, headers, model conversion, and error mapping.
  • Environment configuration: keeps development, staging, and production base URLs out of widget code.
  • Retries: retry transient failures carefully, with a limit and backoff; do not blindly repeat non-idempotent writes.
  • Caching: avoids refetching data that does not need to be fresh on every screen visit.
  • Pagination: follows the API’s cursor or page contract instead of loading an unbounded list.
  • Generated serialization: can reduce repetitive model code when many response types exist, but it is not required for the first request.

These are architectural options, not prerequisites for connecting one REST endpoint. Keep the first working path observable and tested before adding abstractions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

14. Troubleshoot the common symptoms

Symptom Likely cause What to check
Android reports a network error immediately Internet permission is absent from the manifest Confirm <uses-permission android:name="android.permission.INTERNET" /> is outside <application>, then rebuild.
macOS blocks the request Network client entitlement is missing Enable com.apple.security.network.client in the signed macOS target’s entitlements.
The request repeats whenever the screen changes The future is created in build() Store it in initState or trigger a new future only from an explicit refresh action.
A non-JSON error page reaches the model Status was not checked before decoding Reject unexpected status codes first and log the status, not sensitive headers.
jsonDecode succeeds but the model throws The payload shape differs from the documented schema Inspect the decoded type and required keys; update the model only when the API contract has changed.
The UI freezes while loading a large list Parsing is expensive on the main isolate Profile the operation and consider compute for the parsing step.
Tests depend on an Internet connection The function constructs its own client internally Accept an injected http.Client and provide a mock in tests.

15. A beginner’s implementation checklist

  • Run flutter pub add http and import it as http.
  • Confirm the target platform’s network configuration, especially Android permission and macOS entitlement requirements.
  • Use Uri.parse for the endpoint rather than assembling an unchecked string throughout the UI.
  • Keep the request in a Future-returning function.
  • Await the response and check the status code required by that operation.
  • Decode with jsonDecode and validate the top-level JSON type.
  • Convert the payload into a typed Dart model with required-field checks.
  • Create one-time futures in initState, not in build.
  • Render loading, success, empty, and error states intentionally.
  • For writes, set the documented content type, encode the body, and check the operation’s expected 2xx status.
  • For protected endpoints, follow the service’s exact authorization scheme and protect credentials.
  • Inject an http.Client when you need deterministic tests.
  • Move parsing to compute only when response size and profiling justify it.

What to remember

A reliable Flutter REST integration is a small, repeatable pipeline: dependency, platform access, URI, asynchronous request, status validation, JSON decoding, typed model, and explicit UI states. The JSONPlaceholder album demonstrates the mechanics, but your real API documentation remains authoritative for URLs, fields, authentication, pagination, limits, and response codes. Start with the simple typed flow, then add retries, caching, repositories, background parsing, or generated serialization only when the app’s measured needs call for them.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

DartFlutterFutureBuilderhttp packageJSONmobile developmentREST API

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.