π API Design Guide untuk Flutter Starter Kit
(Laravel backend + Flutter frontend, support Sanctum & JWT Redis)
1. Struktur Base URLβ
- Semua API prefiks di
/api/v1
- Contoh:
- Login β
POST /api/v1/login
- Logout β
POST /api/v1/logout
- Refresh (JWT only) β
POST /api/v1/auth/refresh
- User profile β
GET /api/v1/me
- Login β
2. Auth Flow Perbandinganβ
πΉ Sanctumβ
- Token hanya access token β simpan DB
- Tidak ada refresh token.
- Token berlaku lama (atau sesuai config expiration).
- Logout β revoke token di server (
/logout
).
πΉ JWT + Redisβ
- Ada 2 token:
access_token
(umur pendek, ex: 15 menit) +refresh_token
(umur panjang, ex: 7 hari). - Keduanya disimpan di Redis whitelist dengan TTL.
- Access token habis β pakai refresh token ke
/auth/refresh
untuk issue baru. - Logout β hapus token di Redis whitelist.
3. Bentuk Responseβ
Login suksesβ
Sanctum:
{
"data": {
"access_token": "xxx",
"token_type": "Bearer",
"user": {
"id": 1,
"email": "[email protected]",
"name": "John"
}
},
"message": "Login success"
}
JWT:
{
"data": {
"access_token": "xxx",
"refresh_token": "yyy",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"id": 1,
"email": "[email protected]",
"name": "John"
}
},
"message": "Login success",
}
Refresh (JWT only)β
{
"access_token": "new_access",
"refresh_token": "new_refresh",
"token_type": "Bearer",
"expires_in": 900
}
Error formatβ
Auth (OAuth2-style):
{
"error": "invalid_grant",
"error_description": "Invalid credentials"
}
Validation (Laravel default):
{
"message": "The given data was invalid.",
"errors": {
"email": ["The email field is required."]
}
}
4. Flutter Integration Guideβ
Di Flutter Starter Kit kamu buat lapisan auth_repository.dart
β handle login, refresh, logout.
Lalu di dio_interceptor.dart
atur Authorization
header.
πΉ Interceptor Workflowβ
- Cek request path:
/login
,/register
,/forgot-password
β tanpa Authorization.- selain itu β tambahkan
Authorization: Bearer {token}
.
- Kalau request dapat
401 Unauthorized
:- Sanctum β langsung logout (karena tidak ada refresh).
- JWT β coba refresh token β ulangi request. Kalau gagal β logout.
5. Switching Strategyβ
Kalau developer mau ganti dari Sanctum ke JWT:
- Backend:
- Tambah endpoint
/auth/refresh
- Issue refresh token di
/login
- Simpan access + refresh ke Redis
- Tambah endpoint
- Frontend (Flutter):
- Ganti bagian
auth_repository
supaya simpanrefresh_token
di secure storage. - Update
dio_interceptor
untuk handle refresh flow. - Tidak perlu ubah UI atau repository lain.
- Ganti bagian
Kalau mau ganti dari JWT ke Sanctum:
- Backend:
- Hilangkan refresh endpoint.
- Login hanya return
access_token
.
- Frontend:
- Hapus logika refresh token di interceptor.
- Logout cukup hapus access token.
6. Best Practice Checklistβ
- Gunakan
Bearer {token}
standar di header. - Pisahkan error format:
- Auth β pakai
error
,error_description
- Validation β pakai
message
,errors
- Auth β pakai
- Selalu balikan
token_type
di response (biar konsisten). - Simpan token di SecureStorage di Flutter, bukan SharedPrefs.
- Jangan campur user data dengan token di JWT mode (pakai
/me
untuk fetch user). - Di Redis: TTL harus sesuai expiry token β biar auto-expire.
β Response Sukses (Data Object)β
{
"data": {
"id": 1,
"email": "[email protected]",
"name": "John Doe"
},
"errors": {},
"error": null,
"error_description": null,
"message": "Success",
"meta": {}
}
β Response Sukses (Data List)β
{
"data": [
{
"id": 1,
"email": "[email protected]",
"name": "John Doe",
"can": {
"edit": true,
"delete": true,
"publish": true,
},
},
{
"id": 2,
"email": "[email protected]",
"name": "Jane Smith",
"can": {
"edit": true,
"delete": true,
"publish": true,
},
}
],
"errors": {},
"error": null,
"error_description": null,
"message": "List fetched successfully",
"meta": {
"current_page": 1,
"last_page": 10,
"per_page": 20,
"total": 200
}
}
β Response Error Validasi (Laravel-style)β
{
"data": null,
"errors": {
"email": [
"The email field is required.",
"The email must be a valid email address."
],
"password": [
"The password must be at least 8 characters."
]
},
"error": null,
"error_description": null,
"message": "The given data was invalid.",
"meta": {}
}
β Response Error Auth (OAuth2-style / JWT)β
{
"data": null,
"errors": {},
"error": "invalid_grant",
"error_description": "Invalid username or password",
"message": "Authentication failed",
"meta": {}
}
ποΈ Model Konseptual di Flutterβ
class ResponseModel<T> {
final T? data; // Bisa object atau list
final Map<String, List<String>>? errors; // Validasi Laravel
final String? error; // OAuth2/JWT error
final String? errorDescription; // OAuth2/JWT error description
final String? message; // Pesan umum
final Map<String, dynamic>? meta; // Pagination, dsb.
ResponseModel({
this.data,
this.errors,
this.error,
this.errorDescription,
this.message,
this.meta,
});
factory ResponseModel.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) {
return ResponseModel<T>(
data: json['data'] != null ? fromJsonT(json['data']) : null,
errors: (json['errors'] as Map?)?.map(
(key, value) => MapEntry(key as String, List<String>.from(value)),
),
error: json['error'] as String?,
errorDescription: json['error_description'] as String?,
message: json['message'] as String?,
meta: json['meta'] as Map<String, dynamic>?,
);
}
}
Dengan pola ini,
ResponseModel<User>
atauResponseModel<List<User>>
bisa dipakai sama tanpa perlu bikin banyak model response.