HTTP in Flutter: GET, POST, PUT, and DELETE requests using the http package

- Andrés Cruz - ES En español

HTTP in Flutter: GET, POST, PUT, and DELETE requests using the http package

In any modern application, communicating with the Internet is no longer a luxury: it is a requirement. Flutter is no exception. Whether you are building a mobile app for Android and iOS, or a web application with Flutter Web, sooner or later you will need to consume an API or send data to a server. In this tutorial, you will learn exactly how to do it, step by step.

Mobile applications almost always rely on a server to function properly. My Academia app, for example—both the web version built in Vue and the mobile version on Android—makes intensive use of HTTP requests; without that communication layer, the app would simply be useless.

In this article, we will build an example Flutter application that uses the http package to make HTTP requests to a REST API, covering everything from initial setup to error handling and best practices.

In the previous tutorial, we learned how to show sliding modals with showModalBottomSheet in Flutter.

Why do you need HTTP in a Flutter app?

Today, practically any app needs to talk to a server: get data, send forms, update users, validate authentication… and Flutter is no exception. In fact, one of the reasons I have been using Flutter for years is precisely because it integrates REST API workflows so well, regardless of whether the app runs on mobile or web.

Real-world usage examples

In my projects, the most common scenario is consuming APIs built with Laravel, CodeIgniter, or Django. If you have followed my courses, you will have noticed that practically everything we do involves HTTP connections to interconnect systems. Flutter makes it very easy thanks to its http package, and the Academia app itself is a concrete example of this.

Principles of the HTTP Protocol

HTTP (HyperText Transfer Protocol) is the protocol through which the "client" (your Flutter app) and the "server" (your API) communicate. Every message the app sends is an HTTP request, and the API responds with data, usually in JSON format.

There are five fundamental HTTP methods in REST: GET (read), POST (create), PUT (replace), PATCH (partially update), and DELETE (delete). Knowing when to use each one is key to consuming any API correctly.

Flutter supports all of these methods through the http package, allowing you to connect with backends built in Laravel, CodeIgniter, Django, Express, or any technology that exposes an HTTP API.

Configuring your Flutter project to make HTTP requests

Unlike other frameworks, Flutter does not include a production-ready native HTTP client out of the box. You need to install the http package from pub.dev. The process is quick and takes only three steps.

Step 1: Add the http package to pubspec.yaml

Head over to pub.dev/packages/http to check the latest available version. This package allows you to easily make HTTP requests to any API, including those you build yourself.

dependencies:
  http: ^1.2.0

Make sure to specify the most recent version. Then run:

$ flutter pub get

Step 2: Import the package in your Dart file

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

The as http alias is important: it allows you to reference all package methods using the http. prefix, avoiding name collisions with other classes in your project. For example, http.get(), http.post(), http.put(), etc.

Step 3: Your first HTTP request

The following snippet shows the basic pattern you will use in almost all your requests. Notice how each part works:

  • First, you define the URL using Uri.parse(), which converts the URL String into a Uri type that the package can process correctly.
  • Then, you perform the request (in this case http.post()) using await, as it is an asynchronous operation whose duration cannot be predetermined.
  • Finally, the response variable contains the complete server response: the statusCode, headers, and the body containing the data.
var url = Uri.parse('https://your-site.com/endpoint');
var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'});
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');

HTTP Request Types in Flutter

The most common REST APIs work with five methods: GET, POST, PUT, PATCH, and DELETE. Here is how to use each one with the http package.

GET — Fetch data

The GET method is the most widely used. It serves to read or list resources from the server without modifying anything. I use GET requests most often when loading data at screen startup, for example, to fetch a list of products.

final res = await http.get(Uri.parse("http://10.0.2.2:1337/products"));
final List<dynamic> data = json.decode(res.body);

POST — Send data

The POST method is used to create new resources or for actions like logging in. It is normal for the API to respond with a JWT token that you will later use in the Authorization header of other requests.

final res = await http.post(
  Uri.parse('http://10.0.2.2:1337/auth/local'),
  body: {
    "identifier": _emailController.text,
    "password": _passwordController.text,
  },
);

PUT / PATCH — Update resources

Use PUT to replace an entire resource or PATCH for a partial update. In the following example, I combine the body with the headers to pass both the data and the authentication token in the same request:

final res = await http.put(
  Uri.parse("http://10.0.2.2:1337/favorites/${user.favoriteId}"),
  body: {
    "products": json.encode(productsFavoriteId),
  },
  headers: {
    "Authorization": "Bearer ${user.jwt}"
  },
);

DELETE — Delete resources

The DELETE method is the most straightforward. Simply point to the resource you want to remove:

await http.delete(Uri.parse('https://api.com/items/1'));

Working with JSON: json.decode and json.encode

The server always returns plain text in the body. To convert it into a usable Dart structure, you need to import dart:convert and use json.decode():

import 'dart:convert';

final resData = json.decode(res.body);

Similarly, when you need to send lists or maps as part of the body, convert them first to a String with json.encode():

json.encode(myMap);

Map JSON to Dart Models

In development, it is fine to print the body directly to debug, but in production, you should always map the response to Dart classes using fromJson() and toJson() methods. This makes the code more robust, maintainable, and less prone to runtime errors when the API modifies a field.

Error Handling and Best Practices

Always, without exception, check the statusCode before processing the response. Status codes like 401, 403, 404, or 500 occur more frequently than you might imagine, and without this check, it is very difficult to detect what went wrong:

if (res.statusCode == 200) {
  final data = json.decode(res.body);
} else {
  print("Error: ${res.statusCode}");
}

As a personal recommendation: the first time you make API changes and something fails, the first thing you should check is the status code. It is the fastest and most reliable indicator of what is happening.

Timeout and Security with Headers

The http package allows chaining a .timeout() directly onto the request to prevent your app from waiting indefinitely if the server fails to respond:

await http.get(url).timeout(Duration(seconds: 10));

To protect private endpoints, I almost always use Bearer tokens in the Authorization header. This pattern is standard in APIs that implement JWT or Sanctum authentication:

headers: {
  "Authorization": "Bearer ${user.jwt}",
  "Content-Type": "application/json",
}

Debugging and Monitoring HTTP Requests

Tools I use on a daily basis

During development, I frequently rely on these three tools to diagnose request issues:

  • print() to inspect the body and statusCode directly in the console.
  • Postman to test the API independently and isolate whether the issue lies in Flutter or in the backend.
  • Manual URL checking: many errors stem from a misplaced trailing slash or an incorrect endpoint.

Once my API was "mysteriously" failing in production. After much troubleshooting, the problem turned out to be a malformed JSON in the server's response. A simple print(res.body) exposed it instantly.

Complete Example: Consuming a REST API in Flutter

Here is the structure I use in my real-world projects: a centralized service encapsulating requests and a widget consuming it via FutureBuilder.

HTTP Service Class

class ApiService {
  final String baseUrl = "http://10.0.2.2:1337";

  Future<dynamic> getProducts() async {
    final res = await http.get(Uri.parse("$baseUrl/products"));
    if (res.statusCode == 200) {
      return json.decode(res.body);
    } else {
      throw Exception("Error fetching products: ${res.statusCode}");
    }
  }
}

Usage in a Widget with FutureBuilder

FutureBuilder(
  future: ApiService().getProducts(),
  builder: (_, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    }
    if (snapshot.hasError) return Text("Error: ${snapshot.error}");
    final data = snapshot.data;
    return ListView.builder(
      itemCount: data.length,
      itemBuilder: (_, i) => Text(data[i]["name"]),
    );
  },
)

Below are a few additional examples from my Flutter course showing more specific scenarios.

Example 1 — PUT Request with body and headers: updating a record by passing the ID in the URL, a product list in the body, and the auth token in the headers:

final res = await http.put(
  Uri.parse("http://10.0.2.2:1337/favorites/${user.favoriteId}"),
  body: {
    "products": json.encode(productsFavoriteId),
  },
  headers: {
    "Authorization": "Bearer ${user.jwt}"
  },
);

Example 2 — Authenticated GET Request: requesting a protected resource by passing the token in the header and then validating the response with status code 200:

final res = await http.get(
  Uri.parse("http://10.0.2.2:1337/favorites/${user.favoriteId}"),
  headers: {"Authorization": "Bearer ${user.jwt}"},
);
print(res.statusCode);
if (res.statusCode == 200) {
  final resData = json.decode(res.body);
}

Example 3 — POST Request with credentials: sending login credentials in the body to authenticate the user:

final res = await http.post(
  Uri.parse('http://10.0.2.2:1337/auth/local'),
  body: {
    "identifier": _emailController.text,
    "password": _passwordController.text,
  },
);

In all cases, remember that HTTP requests are asynchronous operations. Any access to resources external to the app, whether the internet, the file system, or a local database, is performed using async/await so as not to block Flutter's main thread.

Common errors and how to resolve them

  • CORS
    • If you use Flutter Web, the browser will block requests to different domains unless the server has the Access-Control-Allow-Origin header enabled. Configure CORS on your backend API.
  • Malformed JSON
    • When something "doesn't add up", print the body directly with print(res.body) before trying to decode it. Many times the server returns an HTML error page instead of JSON.
  • Invalid or expired token (401 Unauthorized)
    • If you receive a 401, most likely the token has expired or the Authorization header is not being sent properly. Refresh the token or check the Bearer <token> format.

Additional tips

  • Caching responses
    • Ideal for apps that load catalogs or lists that do not change frequently. It prevents unnecessary requests and improves user experience on slow networks.
  • When to switch to dio?
    • The http package is sufficient for most projects. However, when you need interceptors, request cancellation, multipart file upload support, or automatic retries, consider migrating to dio, which offers a more comprehensive API.
  • When to use WebSockets?
    • When you need real-time data, such as a chat, order tracking, or push notifications. HTTP is request-response based; WebSockets keep a two-way connection open.

Conclusion

Making HTTP requests in Flutter is simple, flexible, and powerful thanks to the http package. With it, you can consume REST APIs, handle authentication with Bearer tokens, send and receive data in JSON format, and debug errors with relative ease.

When I started working with Flutter, one of the first things I noticed is that requests are completely asynchronous, just like in other modern frameworks like React Native or Vue. This prevents interface freezes and keeps your app smooth while receiving data from the Internet.

Frequently Asked Questions (FAQ)

  • Which package do I use to make HTTP requests in Flutter?
    • The official package is http from pub.dev. For advanced cases (interceptors, cancellation, multipart), dio is the most popular alternative.
  • How do I decode JSON responses in Flutter?
    • Import dart:convert and use json.decode(response.body) to convert plain text into a Dart Map or List.
  • How do I send authentication in an HTTP request?
    • Add the "Authorization": "Bearer <your_token>" header to the headers parameter of any method in the http package.
  • When do I use GET vs POST?
    • GET to read or list resources without modifying them; POST to create new resources or for actions like login where you send credentials.
  • Does the http package work on Flutter Web?
    • Yes, it works on mobile, web, and desktop. Just remember that on Flutter Web, you must ensure your API has CORS configured properly.

Now that you know how to make HTTP requests, learn how to persist that data locally in Flutter with SharedPreferences.

Learn how to use the http package in Flutter to make GET, POST, PUT, and DELETE requests to a REST API. Includes examples covering Bearer token authentication headers, JSON handling, and best practices.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.