Lewati ke konten utama

API Sanctum (Mobile Ready)

Catatan: Starter kit ini memakai Laravel Sanctum untuk autentikasi API mobile dan integrasi client luar. Fokus implementasinya adalah sederhana, typed, dan enak dipakai oleh frontend seperti Flutter.

Kenapa Sanctum

Saya sengaja memilih Sanctum sebagai default karena:

  • solusi resmi Laravel
  • lebih sederhana dirawat dibanding JWT
  • cocok untuk mobile app yang memakai bearer token
  • logout cukup dengan revoke token saat ini
  • mendukung token ability yang jelas

Untuk kebutuhan starter kit, ini biasanya lebih waras daripada langsung membawa kompleksitas refresh token dan token lifecycle ala JWT.

Base URL dan Versi API

API menggunakan URL versioning dan saat ini semua endpoint public ada di prefix:

/api/v1

Contoh base URL lokal jika memakai Herd:

http://filament-starter-kit.test/api/v1

Format Response JSON

Starter kit ini memakai kontrak response yang konsisten dan ramah untuk Flutter:

  • message selalu string
  • data hanya muncul jika ada payload sukses
  • errors hanya muncul jika ada error
  • meta hanya muncul jika endpoint memang mengembalikan metadata tambahan, misalnya pagination

Jika data, errors, atau meta tidak ada, key tersebut memang sengaja tidak dikirim.

Untuk success response, implementasi sekarang mengikuti gaya Laravel Resource response. Jadi controller langsung me-return UserResource atau UserCollection, lalu menambahkan message lewat ->additional(...).

Aturan kontraknya sekarang sederhana:

  • data = object untuk endpoint detail atau single resource
  • data = array untuk endpoint collection/list
  • meta = object tambahan, misalnya pagination

Field can untuk Frontend

Agar Flutter bisa menyembunyikan tombol seperti gaya Filament atau Inertia, API mengirim hasil final authorization dalam bentuk boolean can.

Prinsipnya:

  • frontend tidak perlu menebak dari role
  • frontend cukup baca boolean can
  • keputusan akhir tetap datang dari backend
  • can dihitung dari kombinasi token ability Sanctum dan policy

Contoh untuk single item:

{
"message": "User retrieved successfully.",
"data": {
"id": "2f4f4ad8-5320-4f66-8bc4-e8f5d1b6fcb0",
"name": "Kaesa",
"email": "[email protected]",
"avatar": null,
"created_at": "2026-03-27T10:15:30+00:00",
"updated_at": "2026-03-27T10:15:30+00:00",
"can": {
"view": true,
"update": false,
"delete": false
}
}
}

Contoh untuk list:

{
"message": "Users retrieved successfully.",
"data": [
{
"id": "2f4f4ad8-5320-4f66-8bc4-e8f5d1b6fcb0",
"name": "Kaesa",
"email": "[email protected]",
"avatar": null,
"created_at": "2026-03-27T10:15:30+00:00",
"updated_at": "2026-03-27T10:15:30+00:00",
"can": {
"view": true,
"update": false,
"delete": false
}
}
],
"meta": {
"pagination_type": "page",
"current_page": 1,
"per_page": 10,
"total": 25,
"last_page": 3,
"from": 1,
"to": 10,
"has_more_pages": true,
"can": {
"create": true
}
}
}

Dengan ini, frontend tinggal melakukan hal seperti:

  • tampilkan tombol edit jika data.can.update == true
  • tampilkan tombol delete jika data.can.delete == true
  • tampilkan tombol create di halaman list jika meta.can.create == true

Aturan Typing untuk Flutter

Supaya aman dipakai di Dart yang ketat terhadap tipe data, API ini mengikuti aturan berikut:

  • UUID tetap string
  • integer tetap number, bukan string
  • boolean tetap boolean
  • field nullable tetap null
  • timestamp dikirim sebagai string ISO-8601
  • response list dan detail tidak memakai serialisasi model mentah
  • semua nilai can.* selalu boolean

Artinya, frontend tidak perlu menebak apakah per_page itu number atau string.

Authentication Flow

Alur dasarnya:

  1. client login atau register
  2. API mengembalikan Sanctum token
  3. client menyimpan token
  4. request berikutnya mengirim bearer token
  5. logout akan mencabut token yang sedang dipakai

Header yang dipakai:

Authorization: Bearer {token}
Accept: application/json
Content-Type: application/json

Endpoint Auth

POST /api/v1/register

Body:

{
"name": "Flutter User",
"email": "[email protected]",
"password": "password123",
"password_confirmation": "password123",
"device_name": "pixel-8"
}

Contoh response:

{
"message": "User registered successfully.",
"data": {
"user": {
"id": "2f4f4ad8-5320-4f66-8bc4-e8f5d1b6fcb0",
"name": "Flutter User",
"email": "[email protected]",
"avatar": null,
"created_at": "2026-03-27T10:15:30+00:00",
"updated_at": "2026-03-27T10:15:30+00:00"
},
"token": "1|plainTextTokenExample",
"token_type": "Bearer",
"abilities": [
"profile:read"
]
}
}

POST /api/v1/login

Body:

{
"email": "[email protected]",
"password": "password123",
"device_name": "pixel-8"
}

Contoh response:

{
"message": "Login successful.",
"data": {
"user": {
"id": "2f4f4ad8-5320-4f66-8bc4-e8f5d1b6fcb0",
"name": "Flutter User",
"email": "[email protected]",
"avatar": null,
"created_at": "2026-03-27T10:15:30+00:00",
"updated_at": "2026-03-27T10:15:30+00:00"
},
"token": "2|plainTextTokenExample",
"token_type": "Bearer",
"abilities": [
"profile:read",
"users:read"
]
}
}

GET /api/v1/me

Butuh ability token profile:read.

Contoh response:

{
"message": "Authenticated user retrieved successfully.",
"data": {
"id": "2f4f4ad8-5320-4f66-8bc4-e8f5d1b6fcb0",
"name": "Flutter User",
"email": "[email protected]",
"avatar": null,
"created_at": "2026-03-27T10:15:30+00:00",
"updated_at": "2026-03-27T10:15:30+00:00"
}
}

POST /api/v1/logout

Contoh response:

{
"message": "Logout successful."
}

Endpoint User

Endpoint user memakai REST style:

  • GET /api/v1/users
  • POST /api/v1/users
  • GET /api/v1/users/{user}
  • PUT /api/v1/users/{user}
  • PATCH /api/v1/users/{user}
  • DELETE /api/v1/users/{user}

Semua endpoint di atas:

  • butuh bearer token Sanctum
  • tetap melewati policy/gate
  • ability token dicek di API controller

Untuk endpoint detail atau mutation user, data langsung berisi object user. Untuk endpoint list user, data langsung berisi array user dan metadata pagination ada di top-level meta.

Dual Pagination untuk Mobile

Endpoint list user mendukung dua mode:

  • default page pagination
  • optional cursor pagination

Query parameter yang dipakai:

  • pagination=page|cursor
  • per_page
  • page hanya untuk page mode
  • cursor hanya untuk cursor mode

Jika pagination tidak dikirim, default-nya adalah:

pagination=page

Page Pagination

Request:

GET /api/v1/users?per_page=10

Contoh response:

{
"message": "Users retrieved successfully.",
"data": [
{
"id": "2f4f4ad8-5320-4f66-8bc4-e8f5d1b6fcb0",
"name": "Kaesa",
"email": "[email protected]",
"avatar": null,
"created_at": "2026-03-27T10:15:30+00:00",
"updated_at": "2026-03-27T10:15:30+00:00",
"can": {
"view": true,
"update": false,
"delete": false
}
}
],
"meta": {
"pagination_type": "page",
"current_page": 1,
"per_page": 10,
"total": 25,
"last_page": 3,
"from": 1,
"to": 10,
"has_more_pages": true,
"can": {
"create": true
}
}
}

Cursor Pagination

Request:

GET /api/v1/users?pagination=cursor&per_page=10

Contoh response:

{
"message": "Users retrieved successfully.",
"data": [
{
"id": "2f4f4ad8-5320-4f66-8bc4-e8f5d1b6fcb0",
"name": "Kaesa",
"email": "[email protected]",
"avatar": null,
"created_at": "2026-03-27T10:15:30+00:00",
"updated_at": "2026-03-27T10:15:30+00:00",
"can": {
"view": true,
"update": false,
"delete": false
}
}
],
"meta": {
"pagination_type": "cursor",
"per_page": 10,
"next_cursor": "eyJpZCI6IjJmNGY0YWQ4LTUzMjAtNGY2Ni04YmM0LWU4ZjVkMWI2ZmNiMCIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"prev_cursor": null,
"has_more_pages": true,
"can": {
"create": false
}
}
}

Kapan Pilih Page vs Cursor

  • pilih page pagination jika UI butuh nomor halaman
  • pilih cursor pagination jika UI mobile lebih fokus ke infinite scroll

Karena keduanya memakai endpoint yang sama, frontend mobile cukup mengganti query parameter tanpa mengubah model item user.

Cara Coba dengan cURL

Register

curl --request POST \
--url http://filament-starter-kit.test/api/v1/register \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"name": "Flutter User",
"email": "[email protected]",
"password": "password123",
"password_confirmation": "password123",
"device_name": "pixel-8"
}'

Login

curl --request POST \
--url http://filament-starter-kit.test/api/v1/login \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"email": "[email protected]",
"password": "password123",
"device_name": "pixel-8"
}'

Ambil profile user saat ini

curl --request GET \
--url http://filament-starter-kit.test/api/v1/me \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN'

List user dengan pagination default

curl --request GET \
--url 'http://filament-starter-kit.test/api/v1/users?per_page=5' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN'

List user dengan cursor pagination

curl --request GET \
--url 'http://filament-starter-kit.test/api/v1/users?pagination=cursor&per_page=5' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN'

Cara Pakai dari Flutter

Minimal, frontend cukup menyimpan:

  • token
  • token_type
  • abilities

Lalu kirim header:

Authorization: Bearer {token}
Accept: application/json

Untuk list users:

  • pakai tanpa query pagination jika ingin mode default
  • pakai pagination=cursor jika screen memakai infinite scroll
  • baca item list langsung dari data
  • baca metadata pagination dari meta
  • baca meta.pagination_type agar parsing meta jelas
  • baca meta.can.create untuk menentukan apakah tombol create ditampilkan
  • baca data[index].can.update atau data[index].can.delete untuk action per row

Untuk detail user:

  • baca data.can.update untuk tombol edit
  • baca data.can.delete untuk tombol delete

Catatan Error di Local vs Production

Jika Anda mencoba endpoint dengan method yang salah saat local dan APP_DEBUG=true, Laravel masih bisa menampilkan detail exception seperti:

  • exception
  • file
  • line
  • trace

Ini masih berguna untuk debugging lokal.

Tetapi untuk production, pastikan APP_DEBUG=false. Dalam kondisi itu, detail sensitif seperti stack trace tidak boleh diekspos ke client.

Artinya, kalau Anda melihat trace saat development lokal, itu masih normal. Yang penting adalah environment production tidak berjalan dengan debug aktif.

HTTP Status Code yang Dipakai

Starter kit ini memakai status code REST yang umum:

  • 200 OK untuk read, update, logout
  • 201 Created untuk create dan register
  • 401 Unauthorized untuk belum login atau credential salah
  • 403 Forbidden untuk token ability/policy tidak mengizinkan
  • 404 Not Found untuk resource atau route yang tidak ada
  • 405 Method Not Allowed untuk method yang salah pada endpoint yang benar
  • 422 Unprocessable Entity untuk validasi gagal

Catatan Arsitektur

Supaya struktur project tetap bersih:

  • API controller menangani orchestration dan cek ability token
  • Form Request menangani validasi dan authorization yang reusable
  • Action menangani logika bisnis
  • API Resource menangani transformasi output
  • field can dihitung di controller dari token ability + policy, lalu dikirim ke resource

Jadi, action tidak bertugas mengecek ability token.

Referensi