REST APIs
Understanding RESTful APIs — the architecture behind modern web services and how to consume them in Flutter.
REST (Representational State Transfer) is an architectural style for designing networked applications. REST APIs are the most common way for mobile apps to communicate with servers over the internet.
🌐 The Big Idea
A REST API exposes resources (like users, posts, or products) through URLs (endpoints). Your Flutter app sends HTTP requests to these endpoints to fetch, create, update, or delete data.
REST APIs follow a set of principles that make them scalable , stateless , and cacheable . When you build a Flutter app that talks to a backend, you're almost always working with a REST API.
🔑 REST Principles
- Stateless – Each request contains all the information needed to process it
- Cacheable – Responses can be cached to improve performance
- Client-Server – The client (your app) and server are separate
- Uniform Interface – Consistent API design using standard HTTP methods
- Layered System – The API can be composed of multiple layers
An endpoint is a specific URL where your app can access a resource. For example, the JSONPlaceholder API provides these endpoints:
The full URL combines the base URL with the endpoint path :
https://jsonplaceholder.typicode.com + /albums/1
└─────────────── Base URL ───────────────┘ └─ Path ─┘
https://jsonplaceholder.typicode.com/albums/1
REST APIs use different HTTP methods for different operations. Here's how to make each type of request
in Flutter using the
http
package.
import
'dart:convert'
;
import
'package:http/http.dart'
as
http;
class
ApiService
{
final
String baseUrl =
'https://jsonplaceholder.typicode.com'
;
final
http.Client client;
ApiService
({http.Client? client}) : client = client ?? http.Client();
// 1. GET - Fetch data
Future
<Map<String, dynamic>>
get
(String endpoint) async {
final
response = await client.get(
Uri.parse(
'$baseUrl$endpoint'
),
headers: {
'Accept'
:
'application/json'
},
);
if
(response.statusCode ==
200
) {
return
jsonDecode(response.body)
as
Map<String, dynamic>;
}
else
{
throw
Exception(
'GET failed: ${response.statusCode}'
);
}
}
// 2. POST - Create new data
Future
<Map<String, dynamic>>
post
(String endpoint, Map<String, dynamic> data) async {
final
response = await client.post(
Uri.parse(
'$baseUrl$endpoint'
),
headers: {
'Accept'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
},
body: jsonEncode(data),
);
if
(response.statusCode ==
201
) {
return
jsonDecode(response.body)
as
Map<String, dynamic>;
}
else
{
throw
Exception(
'POST failed: ${response.statusCode}'
);
}
}
// 3. PUT - Update existing data (full replace)
Future
<Map<String, dynamic>>
put
(String endpoint, Map<String, dynamic> data) async {
final
response = await client.put(
Uri.parse(
'$baseUrl$endpoint'
),
headers: {
'Accept'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
},
body: jsonEncode(data),
);
if
(response.statusCode ==
200
) {
return
jsonDecode(response.body)
as
Map<String, dynamic>;
}
else
{
throw
Exception(
'PUT failed: ${response.statusCode}'
);
}
}
// 4. DELETE - Remove data
Future
<
void
>
delete
(String endpoint) async {
final
response = await client.delete(
Uri.parse(
'$baseUrl$endpoint'
),
headers: {
'Accept'
:
'application/json'
},
);
if
(response.statusCode !=
200
&& response.statusCode !=
204
) {
throw
Exception(
'DELETE failed: ${response.statusCode}'
);
}
}
}
client.get()
to fetch data. Always set the
Accept
header to
application/json
.
client.post()
with a JSON-encoded body.
Set both
Accept
and
Content-Type
headers.
client.put()
to update existing resources.
Like POST, you need to send JSON data in the body.
client.delete()
to remove resources.
Success codes are 200 (OK) or 204 (No Content).
Here's a complete example that demonstrates fetching, creating, and updating albums using a REST API.
import
'dart:convert'
;
import
'package:flutter/material.dart'
;
import
'package:http/http.dart'
as
http;
class
Album
{
final
int
userId;
final
int
id;
final
String title;
const
Album({
required
this
.userId,
required
this
.id,
required
this
.title,
});
Map<String, dynamic>
toJson
() => {
'userId'
: userId,
'id'
: id,
'title'
: title,
};
factory Album.fromJson(Map<String, dynamic> json) {
return
Album(
userId: json[
'userId'
]
as
int
,
id: json[
'id'
]
as
int
,
title: json[
'title'
]
as
String,
);
}
}
// API Service
class
AlbumService
{
final
String baseUrl =
'https://jsonplaceholder.typicode.com'
;
final
http.Client client;
AlbumService
({http.Client? client}) : client = client ?? http.Client();
// GET all albums
Future
<List<Album>>
fetchAlbums
() async {
final
response = await client.get(
Uri.parse(
'$baseUrl/albums'
),
headers: {
'Accept'
:
'application/json'
},
);
if
(response.statusCode ==
200
) {
final
List data = jsonDecode(response.body)
as
List;
return
data.map((json) => Album.fromJson(json)).toList();
}
else
{
throw
Exception(
'Failed to fetch albums: ${response.statusCode}'
);
}
}
// POST - Create new album
Future
<Album>
createAlbum
(String title) async {
final
response = await client.post(
Uri.parse(
'$baseUrl/albums'
),
headers: {
'Accept'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
},
body: jsonEncode({
'title'
: title,
'userId'
:
1
,
}),
);
if
(response.statusCode ==
201
) {
return
Album.fromJson(jsonDecode(response.body)
as
Map<String, dynamic>);
}
else
{
throw
Exception(
'Failed to create album: ${response.statusCode}'
);
}
}
// PUT - Update album
Future
<Album>
updateAlbum
(
int
id, String newTitle) async {
final
response = await client.put(
Uri.parse(
'$baseUrl/albums/$id'
),
headers: {
'Accept'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
},
body: jsonEncode({
'id'
: id,
'title'
: newTitle,
'userId'
:
1
,
}),
);
if
(response.statusCode ==
200
) {
return
Album.fromJson(jsonDecode(response.body)
as
Map<String, dynamic>);
}
else
{
throw
Exception(
'Failed to update album: ${response.statusCode}'
);
}
}
// DELETE - Delete album
Future
<
void
>
deleteAlbum
(
int
id) async {
final
response = await client.delete(
Uri.parse(
'$baseUrl/albums/$id'
),
headers: {
'Accept'
:
'application/json'
},
);
if
(response.statusCode !=
200
&& response.statusCode !=
204
) {
throw
Exception(
'Failed to delete album: ${response.statusCode}'
);
}
}
}
// Flutter UI
void
main
() => runApp(
MyApp
());
class
MyApp
extends
StatelessWidget {
@override
Widget
build
(BuildContext context) {
return
MaterialApp(
title:
'REST API Example'
,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: AlbumListScreen(),
);
}
}
class
AlbumListScreen
extends
StatefulWidget {
@override
State<AlbumListScreen>
createState
() => _AlbumListScreenState();
}
class
_AlbumListScreenState
extends
State<AlbumListScreen> {
late
Future<List<Album>> futureAlbums;
final
AlbumService _service = AlbumService();
@override
void
initState
() {
super
.initState();
futureAlbums = _service.fetchAlbums();
}
void
_refreshAlbums
() {
setState(() {
futureAlbums = _service.fetchAlbums();
});
}
Future
<
void
>
_showCreateDialog
() {
final
TextEditingController controller = TextEditingController();
return
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Create Album'
),
content: TextField(
controller: controller,
decoration: InputDecoration(
hintText:
'Enter album title...'
,
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancel'
),
),
ElevatedButton(
onPressed: () async {
if
(controller.text.isNotEmpty) {
try
{
final
newAlbum = await _service.createAlbum(controller.text);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(
'Created: ${newAlbum.title}'
)),
);
_refreshAlbums();
Navigator.pop(context);
}
catch
(e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(
'Error: $e'
), backgroundColor: Colors.red),
);
}
}
},
child: Text(
'Create'
),
),
],
),
);
}
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(
title: Text(
'Albums'
),
centerTitle:
true
,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _refreshAlbums,
),
],
),
body: FutureBuilder<List<Album>>(
future: futureAlbums,
builder: (context, snapshot) {
if
(snapshot.connectionState == ConnectionState.waiting) {
return
Center(child: CircularProgressIndicator());
}
if
(snapshot.hasError) {
return
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size:
64
, color: Colors.red),
SizedBox(height:
16
),
Text(
'Error: ${snapshot.error}'
),
SizedBox(height:
16
),
ElevatedButton(
onPressed: _refreshAlbums,
child: Text(
'Retry'
),
),
],
),
);
}
if
(!snapshot.hasData || snapshot.data!.isEmpty) {
return
Center(child: Text(
'No albums found'
));
}
final
albums = snapshot.data!;
return
ListView.builder(
itemCount: albums.length,
itemBuilder: (context, index) {
final
album = albums[index];
return
ListTile(
leading: CircleAvatar(
child: Text(album.id.toString()),
),
title: Text(album.title),
subtitle: Text(
'User ${album.userId}'
),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () async {
try
{
await _service.deleteAlbum(album.id);
_refreshAlbums();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(
'Deleted: ${album.title}'
)),
);
}
catch
(e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(
'Error: $e'
), backgroundColor: Colors.red),
);
}
},
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _showCreateDialog,
child: Icon(Icons.add),
),
);
}
}
🔑 Common Patterns
-
Resource Collections
–
/usersreturns a list of users -
Individual Resources
–
/users/1returns user with ID 1 -
Nested Resources
–
/users/1/postsreturns posts by user 1 -
Pagination
–
/posts?page=2&limit=10for paginated results -
Filtering
–
/posts?userId=1filters posts by user -
Search
–
/posts?q=fluttersearches for "flutter"
// List all resources
GET /api/users
// Get all users
// Get a specific resource
GET /api/users/123
// Get user with ID 123
// Create a new resource
POST /api/users
// Create a new user
// Update a resource (full replace)
PUT /api/users/123
// Replace user 123 entirely
// Update a resource (partial)
PATCH /api/users/123
// Update specific fields of user 123
// Delete a resource
DELETE /api/users/123
// Delete user 123
// Nested resources
GET /api/users/123/posts
// Get posts by user 123
// Filtering
GET /api/posts?userId=123
// Filter posts by user ID
// Pagination
GET /api/posts?page=2&limit=10
// Get page 2 with 10 items per page
// Search
GET /api/posts?q=flutter
// Search for "flutter" in posts
Only checking for 200 status codes misses other success codes like 201 (Created) and 204 (No Content).
Check for
200
,
201
, and
204
as success codes.
Throw exceptions for any other status code.
When sending data with POST or PUT, you must set
Content-Type: application/json
so the server knows how to parse your request.
Always set
Accept: application/json
and
Content-Type: application/json
when sending or receiving JSON data.
Hardcoding URLs in multiple places makes your code hard to maintain and test.
Store the base URL in a single place and build full URLs dynamically. This makes it easy to change the API endpoint in one place.
🎯 Key Takeaway
REST APIs are the standard way for Flutter apps to communicate with servers. Learn the HTTP methods (GET, POST, PUT, DELETE), understand endpoints and status codes , and always handle loading and error states . Good API design makes your app scalable and maintainable.