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.