Networking & REST API Integration

POST, PUT & DELETE Requests

16 min Lesson 3 of 13

POST, PUT & DELETE Requests

While GET retrieves data, the three mutating verbs — POST, PUT, and DELETE — are what make your app a full participant in a REST API. POST creates new resources, PUT (and PATCH) update existing ones, and DELETE removes them. Each verb has a distinct contract with the server, and getting the details right — correct headers, a properly-encoded body, and careful status-code handling — is what separates reliable apps from brittle ones.

Setting the Content-Type Header

Whenever you send a body to the server, you must declare how that body is encoded via the Content-Type header. REST APIs almost universally expect application/json. Without it, many servers return a 400 Bad Request or silently misparse your payload. The dart:convert library provides jsonEncode() to serialize a Dart Map into a JSON string.

Note: Content-Type: application/json tells the server the body encoding. The companion header Accept: application/json tells the server which format you want back. Always set both for JSON APIs.

POST — Creating a Resource

Use http.post() to create a new resource. The server typically responds with 201 Created and returns the newly created object (often with a server-generated id) in the response body.

POST Request — Creating a New Post

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

Future<Map<String, dynamic>> createPost({
  required String title,
  required String body,
  required int userId,
}) async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/posts');

  final response = await http.post(
    uri,
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      'Accept': 'application/json',
      'Authorization': 'Bearer YOUR_TOKEN',
    },
    body: jsonEncode({
      'title': title,
      'body': body,
      'userId': userId,
    }),
  );

  if (response.statusCode == 201) {
    // Server returns the created object, including the new id
    return jsonDecode(response.body) as Map<String, dynamic>;
  } else {
    throw Exception(
      'POST failed. Status: ${response.statusCode} — ${response.body}',
    );
  }
}

PUT — Replacing a Resource

Use http.put() to fully replace an existing resource. The URI targets the specific resource (e.g. /posts/1). PUT is idempotent — calling it multiple times with the same body produces the same result. The server usually responds with 200 OK and the updated representation, or 204 No Content if it returns nothing.

Tip: Use PUT when you are replacing the entire resource (all fields). Use PATCH when you only want to update a subset of fields. Many APIs implement only one of the two — check the documentation.

PUT Request — Replacing a Post

Future<Map<String, dynamic>> updatePost({
  required int postId,
  required String title,
  required String body,
  required int userId,
}) async {
  final uri = Uri.parse(
    'https://jsonplaceholder.typicode.com/posts/$postId',
  );

  final response = await http.put(
    uri,
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      'Accept': 'application/json',
    },
    body: jsonEncode({
      'id': postId,
      'title': title,
      'body': body,
      'userId': userId,
    }),
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body) as Map<String, dynamic>;
  } else if (response.statusCode == 404) {
    throw Exception('Post $postId not found.');
  } else {
    throw Exception(
      'PUT failed. Status: ${response.statusCode}',
    );
  }
}

DELETE — Removing a Resource

Use http.delete() to remove a resource. DELETE requests typically have no body. The server responds with 200 OK (with a confirmation payload), 204 No Content (success, no body), or 404 Not Found if the resource does not exist.

Warning: DELETE is destructive and irreversible on the server side. Always confirm the user's intent with a dialog before calling it, and never call it in a button's onPressed without a guard.

DELETE Request — Deleting a Post

Future<void> deletePost(int postId) async {
  final uri = Uri.parse(
    'https://jsonplaceholder.typicode.com/posts/$postId',
  );

  final response = await http.delete(
    uri,
    headers: {
      'Accept': 'application/json',
      'Authorization': 'Bearer YOUR_TOKEN',
    },
  );

  // 200 or 204 both indicate success
  if (response.statusCode != 200 && response.statusCode != 204) {
    throw Exception(
      'DELETE failed. Status: ${response.statusCode}',
    );
  }
  // On success, nothing is returned — the resource no longer exists
}

Putting It Together — CRUD in a Widget

In a real Flutter widget you wrap each API call in a try/catch, manage a loading flag with setState, and surface errors to the user. Here is a condensed pattern for a "save" button that calls POST or PUT depending on whether you are creating or editing:

Save Button — POST or PUT Based on Edit Mode

class PostFormState extends State<PostForm> {
  bool _isSaving = false;

  Future<void> _save() async {
    setState(() => _isSaving = true);
    try {
      if (widget.postId == null) {
        // Creating a new post
        await createPost(
          title: _titleController.text,
          body: _bodyController.text,
          userId: 1,
        );
      } else {
        // Replacing an existing post
        await updatePost(
          postId: widget.postId!,
          title: _titleController.text,
          body: _bodyController.text,
          userId: 1,
        );
      }
      if (mounted) Navigator.pop(context, true); // return success
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $e')),
        );
      }
    } finally {
      if (mounted) setState(() => _isSaving = false);
    }
  }
}

Summary

In this lesson you learned how to send mutating HTTP requests from Flutter. POST creates resources and expects a 201 response; PUT fully replaces a resource and expects 200 or 204; DELETE removes a resource and also expects 200 or 204. All requests that carry a body require the Content-Type: application/json header and a jsonEncode()-serialised body. Wrap every call in a try/catch, manage a loading state with setState, and always check the status code before acting on the response. In the next lesson you will learn how to handle authentication tokens and refresh flows.