POST, PUT & DELETE Requests
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.
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.
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.
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.