Server Side Dart with Shelf and Supabase
How create CRUD API with Dart Shelf and Supabase
Pengenalan
Shelf
Di tulisan kali ini saya mau bahas lagi mengenai server side menggunakan bahasa Dart. Sebelumnya saya pernah juga membahas mengenai cara membuat CRUD API dengan Dart menggunakan Aqueduct.
Namun, setelah saya lihat ternyata perkembangan dari Aqueduct sudah tidak dilanjutkan lagi. Oleh karena itu, di tulisan ini saya mau bahas lagi mengenai server side menggunakan bahasa Dart tapi, menggunakan plugin yang berbeda pula. Kali ini kita akan mencoba menggunakan plugin yang bernama Shelf.
Jadi, Shelf ini merupakan plugin yang dibuat langsung oleh tim Dart. Dan saat ini saya lihat commit-an terakhir mereka sekitar 17 hari yang lalu. Yang menandakan bahwa ini plugin masih dikembangkan (mudah-mudahan saja 😄)
Tapi, berhubung ini plugin dibuat oleh tim Dart-nya. Saya jadi semakin penasaran gimana cara membuat server side menggunakan plugin tersebut.
Supabase
Nah, kalau bahas server side rasanya kurang lengkap kalau kita tidak bahas juga mengenai database-nya. Jadi, biar lebih kece untuk server side-nya nanti kita akan pakai database miliknya si Supabase ya.
Jadi, Supabase ini hampir mirip seperti Firebase gitu ya. Cuma kalau dilihat dari halaman depannya saat ini mereka bilang kalau Supabase ini salah satu alternatif dari Firebase yang open source. Dan biasanya nih produk-produk yang open source begini prospek kedepannya bakalan cerah apalagi fungsinya hampir mirip atau bahkan melebihi expectation dari produk yang serupa.
Dan setelah saya cek di pub.dev ternyata plugin supabase untuk projek Dart sudah ada yang buat.
Btw, perlu dicatat ya bahwa plugin diatas merupakan plugin untuk projek Dart bukan untuk Flutter. Kalau Supabase untuk Flutter bisa coba yang ini.
Tapi, untuk saat ini kita nggak akan bahas mengenai Supabase untuk Flutter. Fokus kita di tulisan ini adalah bagaimana cara membuat CRUD API menggunakan Shelf dan Supabase.
Pembuatan Projek
Buat Projek Supabase
Untuk langkah pertamanya, kita perlu buat akun terlebih dahulu di Supabase.
Selanjutnya, kita perlu buat sebuah projek di Supabase.
Kemudian, kita perlu isi sebuah form untuk membuat projeknya.
Setelah itu, kita perlu menunggu beberapa menit sampai projeknya benar-benar berhasil dibuat.
Jika sudah selesai seharusnya tampilan dashboard-nya menjadi seperti gambar berikut ini ya.
Buat Table Profile
Masih di Supabase. Kita perlu buat table baru dengan nama profile di Database. Caranya, kita tekan saja menu Database lalu, tekan tombol New.
Kemudian, kita atur kolomnya menjadi seperti gambar berikut.
Oya, pada kolom diatas pastikan yang nullable hanya kolom email dan phone_number. Kemudian, kita coba insert row di table tersebut dengan cara tekan tombol Insert row.
Kemudian, kita isi datanya.
Selanjutnya, tekan Save.
Buat Projek Dart
Oke, sekarang kita beralih ke Dart. Untuk langkah pertama di sisi Dart, kita perlu buat projek baru Dart-nya. Untuk membuatnya kita bisa menggunakan IDE atau command line. Biar seragam saya contohkan menggunakan command line saja ya.
dart create dart_profile
Setelah itu kita buka projeknya.
Kemudian, kita perlu rename dart_profile.dart menjadi main.dart.
Atur pubspec.yaml
Langkah berikutnya kita perlu menambahkan plugin berikut kedalam pubspec.yaml.
name: dart_profile
description: A simple command-line application.
version: 1.0.0
environment:
sdk: '>=2.15.1 <3.0.0'
dependencies:
args: ^2.3.0
shelf: ^1.2.0
shelf_router: ^1.1.2
supabase: ^0.2.14
jiffy: ^5.0.0
json_annotation: ^4.4.0
dev_dependencies:
lints: ^1.0.0
http: ^0.13.0
build_runner: ^2.1.7
json_serializable: ^6.1.4
Selanjutnya, kita jalankan perintah dart pub get
untuk mengunduh dependency-nya.
Buat Class Model
Biar lebih gampang kita akan membuat class model dari table profile. Untuk membuatnya, silakan buat file baru dengan nama profile.dart dan isikan dengan kode berikut.
import 'package:json_annotation/json_annotation.dart';
part 'profile.g.dart';
@JsonSerializable()
class Profile {
@JsonKey(name: 'id')
final int? id;
@JsonKey(name: 'created_at')
final String? createdAt;
@JsonKey(name: 'full_name')
final String? fullName;
@JsonKey(name: 'email')
final String? email;
@JsonKey(name: 'phone_number')
final String? phoneNumber;
@JsonKey(name: 'birth_date')
final String? birthDate;
Profile({
this.id,
this.createdAt,
this.fullName,
this.email,
this.phoneNumber,
this.birthDate,
});
factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
Map<String, dynamic> toJson() => _$ProfileToJson(this);
@override
String toString() {
return 'Profile{id: $id, createdAt: $createdAt, fullName: $fullName, '
'email: $email, phoneNumber: $phoneNumber, birthDate: $birthDate}';
}
}
Kemudian, jalankan perintah dart run build_runner build
untuk membuat file generator-nya. Jika berhasil seharusnya ada 1 file baru yang terbentuk secara otomatis dengan nama profile.g.dart.
Buat Fungsi Supabase
Langkah berikutnya kita akan membuat fungsi CRUD kedalam database Supabase. Caranya, kita buat file baru dengan nama supabase_helper.dart. Kemudian, kita isi kode didalamnya menjadi seperti berikut.
class SupabaseHelper {
final supabaseUrl = '';
final supabaseKey = '';
final profileTableName = 'profile';
late SupabaseClient supabaseClient;
SupabaseHelper() {
supabaseClient = SupabaseClient(supabaseUrl, supabaseKey);
}
Map<String, dynamic> getTemplateErrorMessage({
String title = 'Info',
String message = 'Data not found',
}) {
return {
'title': title,
'message': message,
};
}
Future<Response> getAllProfile() async {
// TODO: Tampilkan semua data profile
return Response.ok('get all profile');
}
Future<Response> getProfileById(String id) async {
// TODO: Tampilkan profile berdasarkan id yang diberikan
return Response.ok('get profile by id');
}
Future<Response> createProfile(String bodyRequest) async {
// TODO: Masukkan data profile baru kedalam table
return Response.ok('create profile');
}
Future<Response> updateProfile(String id, String bodyRequest) async {
// TODO: Update data profile kedalam table
return Response.ok('update profile');
}
Future<Response> deleteProfile(String id) async {
// TODO: Hapus data profile dari table
return Response.ok('delete profile');
}
}
Untuk nilai supabaseUrl dan supabaseKey bisa kamu ambil dari Supabase. Caranya, pilih menu Settings-Project settings-API. Salin nilai anon public kedalam variable supabaseKey. Dan salin juga nilai URL kedalam variable supabaseUrl.
Buat Fungsi Menampilkan Semua Data Profile
Masih didalam file supabase_helper.dart kita akan membuat fungsi yang menampilkan semua data profile. Untuk membuatnya, silakan kita ubah kode berikut.
Future<Response> getAllProfile() async {
// TODO: Tampilkan semua data profile
return Response.ok('get all profile');
}
Menjadi seperti berikut.
Future<Response> getAllProfiles() async {
try {
final response = await supabaseClient.from(profileTableName).select().execute();
late Map<String, dynamic> responseJson;
if (response.data == null) {
responseJson = getTemplateErrorMessage();
return Response.notFound(jsonEncode(responseJson));
}
final listProfiles = List.from(response.data).map((e) => Profile.fromJson(e)).toList();
responseJson = {
'data': listProfiles,
};
return Response.ok(jsonEncode(responseJson));
} catch (error) {
return Response.internalServerError(
body: getTemplateErrorMessage(
title: 'Error',
message: error.toString(),
),
);
}
}
Bisa kita lihat pada kode diatas bahwa kita ada handle untuk beberapa case misal datanya tidak ada, datanya ada dan error-nya pun juga kita tangkap dalam blok try-catch
. Jadi, seumpama ada case yang tidak ter-handle bisa langsung masuk kedalam catch
-nya.
Buat Fungsi Menampilkan Profile Berdasarkan ID
Langkah berikutnya kita akan membuat fungsi yang berguna untuk menampilkan sebuah data profile berdasarkan ID yang diberikan. Untuk membuatnya silakan ubah kode berikut.
Future<Response> getProfileById(String id) async {
// TODO: Tampilkan profile berdasarkan id yang diberikan
return Response.ok('get profile by id');
}
Menjadi seperti berikut.
Future<Response> getProfileById(String id) async {
try {
final response = await supabaseClient.from(profileTableName).select().eq('id', id).execute();
final listProfiles = List.from(response.data).map((e) => Profile.fromJson(e)).toList();
late Map<String, dynamic> responseJson;
if (listProfiles.isEmpty) {
responseJson = getTemplateErrorMessage();
return Response.notFound(jsonEncode(responseJson));
}
responseJson = listProfiles.first.toJson();
return Response.ok(jsonEncode(responseJson));
} catch (error) {
return Response.internalServerError(
body: getTemplateErrorMessage(
title: 'Error',
message: error.toString(),
),
);
}
}
Buat Fungsi Tambah Data Profile
Fungsi ini berguna untuk menambahkan data profile baru kedalam table. Untuk membuatnya ubah kode berikut.
Future<Response> createProfile(String bodyRequest) async {
// TODO: Masukkan data profile baru kedalam table
return Response.ok('create profile');
}
Menjadi seperti berikut.
Future<Response> createProfile(String bodyRequest) async {
try {
final profile = Profile.fromJson(jsonDecode(bodyRequest)).toJson()
..remove('id')
..remove('created_at');
final response = await supabaseClient.from(profileTableName).insert(profile).execute();
late Map<String, dynamic> responseJson;
if (response.data == null) {
responseJson = getTemplateErrorMessage(message: 'Create profile failed');
return Response.internalServerError(body: jsonEncode(responseJson));
}
final listData = List.from(response.data).map((e) => Profile.fromJson(e)).toList();
if (listData.isEmpty) {
responseJson = getTemplateErrorMessage();
return Response.notFound(jsonEncode(responseJson));
}
responseJson = listData.first.toJson();
return Response(201, body: jsonEncode(responseJson));
} catch (error) {
return Response.internalServerError(
body: getTemplateErrorMessage(
title: 'Error',
message: error.toString(),
),
);
}
}
Mungkin kamu bertanya-tanya mengapa respon dari Supabase selalu kita handle dalam bentuk List. Karena, tipe data dari response.data
berbentuk List. Makanya, kita selalu handle-nya dalam bentuk List yang kemudian, kita mapping-kan kedalam class model Profile
.
Buat Fungsi Ubah Data Profile
Fungsi ini berguna untuk mengubah data yang sudah ada atau sering kita sebut dengan edit data. Untuk membuatnya silakan ubah kode berikut.
Future<Response> updateProfile(String id, String bodyRequest) async {
// TODO: Update data profile kedalam table
return Response.ok('update profile');
}
Menjadi seperti berikut.
Future<Response> updateProfile(String id, String bodyRequest) async {
try {
final profile = Profile.fromJson(jsonDecode(bodyRequest)).toJson()
..remove('id')
..remove('created_at');
final response = await supabaseClient.from(profileTableName).update(profile).eq('id', id).execute();
late Map<String, dynamic> responseJson;
if (response.data == null) {
responseJson = getTemplateErrorMessage(message: 'Update profile failed');
return Response.internalServerError(body: jsonEncode(responseJson));
}
final listData = List.from(response.data).map((e) => Profile.fromJson(e)).toList();
if (listData.isEmpty) {
responseJson = getTemplateErrorMessage();
return Response.notFound(jsonEncode(responseJson));
}
responseJson = listData.first.toJson();
return Response.ok(jsonEncode(responseJson));
} catch (error) {
return Response.internalServerError(
body: getTemplateErrorMessage(
title: 'Error',
message: error.toString(),
),
);
}
}
Buat Fungsi Hapus Data Profile
Fungsi ini berguna untuk menghapus data profile yang ada. Untuk membuatnya silakan ubah kode berikut.
Future<Response> deleteProfile(String id) async {
// TODO: Hapus data profile dari table
return Response.ok('delete profile');
}
Menjadi seperti berikut.
Future<Response> deleteProfile(String id) async {
try {
final response = await supabaseClient.from(profileTableName).delete().eq('id', id).execute();
late Map<String, dynamic> responseJson;
if (response.data == null) {
responseJson = getTemplateErrorMessage(message: 'Delete profile failed');
return Response.internalServerError(body: jsonEncode(responseJson));
}
final listData = List.from(response.data).map((e) => Profile.fromJson(e)).toList();
if (listData.isEmpty) {
responseJson = getTemplateErrorMessage();
return Response.notFound(jsonEncode(responseJson));
}
responseJson = listData.first.toJson();
return Response.ok(jsonEncode(responseJson));
} catch (error) {
return Response.internalServerError(
body: getTemplateErrorMessage(
title: 'Error',
message: error.toString(),
),
);
}
}
Buat Routing Endpoint-nya
Setelah fungsi CRUD-nya selesai kita buat. Maka, langkah selanjutnya adalah kita perlu define route endpoint-nya. Untuk membuatnya silakan buat file baru dengan nama profile_controller.dart dan isikan dengan kode berikut.
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'supabase_helper.dart';
class ProfileController {
Handler get handler {
final router = Router();
final supabaseHelper = SupabaseHelper();
router.get('/profile', (Request request) async {
return supabaseHelper.getAllProfiles();
});
router.get('/profile/<id>', (Request request, String id) async {
return supabaseHelper.getProfileById(id);
});
router.post('/profile', (Request request) async {
final strBodyRequest = await request.readAsString();
return supabaseHelper.createProfile(strBodyRequest);
});
router.put('/profile/<id>', (Request request, String id) async {
final strBodyRequest = await request.readAsString();
return supabaseHelper.updateProfile(id, strBodyRequest);
});
router.delete('/profile/<id>', (Request request, String id) async {
return supabaseHelper.deleteProfile(id);
});
router.all('/<ignored|.*>', (Request request) async {
return Response.notFound('Page not found');
});
return router;
}
}
Update File main.dart
Langkah terakhir adalah kita perlu ubah kode didalam file main.dart menjadi seperti berikut.
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'profile_controller.dart';
void main(List<String> arguments) async {
final handler = const Pipeline().addMiddleware(logRequests()).addHandler(ProfileController().handler);
await serve(handler, 'localhost', 8080);
print('Server is running');
}
Setelah selesai semuanya mari kita jalankan programnya menggunakan perintah berikut.
dart run bin/main.dart
Testing
Berikut adalah video hasil testing dari semua endpoint yang sudah kita buat.
Kesimpulan
Jadi kesimpulannya adalah bahwa penggunaan plugin Shelf ini jika kita lihat ternyata cukuplah mudah dan familiar. Untuk Supabase-nya menurut saya bagus ya. Database-nya itu loh mereka adopsi Postgres. Jadi, agak sedikit berbeda dengan database si Firebase yang bentuknya lebih ke NoSQL. Dan Supabase ini menurut saya bisa menjadi alternatif selain dari Firebase. Seperti biasa untuk source code lengkapnya bisa dilihat di Github ya.