Nusanet Developers
Published in

Nusanet Developers

Server Side Dart with Shelf and Supabase

How create CRUD API with Dart Shelf and Supabase

Pengenalan

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 😄)

Commit-an terakhir 17 hari yang lalu

Tapi, berhubung ini plugin dibuat oleh tim Dart-nya. Saya jadi semakin penasaran gimana cara membuat server side menggunakan plugin tersebut.

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.

Halaman depan Supabase

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

Untuk langkah pertamanya, kita perlu buat akun terlebih dahulu di Supabase.

Sign in di Supabase

Selanjutnya, kita perlu buat sebuah projek di Supabase.

Buat projek di Supabase

Kemudian, kita perlu isi sebuah form untuk membuat projeknya.

Form create new project

Setelah itu, kita perlu menunggu beberapa menit sampai projeknya benar-benar berhasil dibuat.

Menunggu projeknya berhasil dibuat

Jika sudah selesai seharusnya tampilan dashboard-nya menjadi seperti gambar berikut ini ya.

Projek berhasil dibuat di Supabase

Masih di Supabase. Kita perlu buat table baru dengan nama profile di Database. Caranya, kita tekan saja menu Database lalu, tekan tombol New.

Buat table baru di Supabase

Kemudian, kita atur kolomnya menjadi seperti gambar berikut.

Buat table baru dengan nama profile

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.

Insert row di table profile

Kemudian, kita isi datanya.

Isi data table profile

Selanjutnya, tekan Save.

Datanya sudah masuk ya

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.

Struktur projek Dart

Kemudian, kita perlu rename dart_profile.dart menjadi main.dart.

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.

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.

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.

Cara mendapatkan nilai dari variable supabaseKey dan supabaseUrl

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.

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(),
),
);
}
}

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 .

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(),
),
);
}
}

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(),
),
);
}
}

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;
}
}

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

Berikut adalah video hasil testing dari semua endpoint yang sudah kita buat.

Testing 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.

--

--

Stories and insights from the developers in Nusanet

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store