Aplikasi Laravel Anda sudah berjalan di browser, lalu datang permintaan baru: tim mobile butuh endpoint untuk login dan mengambil data produk. Session dan cookie bawaan tidak cocok untuk aplikasi Android atau iOS, sementara OAuth2 penuh terasa berlebihan untuk kebutuhan sesederhana itu. Di sinilah Laravel Sanctum berguna.
Tutorial ini membangun REST API di Laravel 11 dari awal: register, login, logout, route terproteksi, pembatasan hak token, rate limiting, dan format JSON yang konsisten. Di setiap langkah ada penjelasan kenapa langkah itu perlu, bukan hanya kodenya.
Kapan Memakai Sanctum, Kapan Tidak
Sanctum punya dua mode. Mode API token menerbitkan token acak yang disimpan di tabel personal_access_tokens, cocok untuk aplikasi mobile dan integrasi server. Mode SPA authentication memakai cookie session untuk frontend React/Vue yang berada di domain yang sama. Artikel ini fokus pada mode token.
| Kebutuhan | Pilihan yang cocok | Alasan |
|---|---|---|
| Aplikasi mobile milik sendiri | Sanctum (token) | Ringan, token bisa dicabut per perangkat |
| SPA di domain yang sama | Sanctum (cookie) | Terlindungi CSRF, token tidak disimpan di JavaScript |
| Pihak ketiga login atas nama user Anda | Passport (OAuth2) | Butuh alur authorization code dan client terdaftar |
| Komunikasi antar-server internal | Sanctum token dengan abilities terbatas | Cukup satu token per layanan |
Jika Anda masih menimbang bentuk API-nya, baca juga perbandingan REST API vs GraphQL sebelum mulai.
Langkah 1: Instalasi
Di Laravel 11, file routes/api.php tidak lagi ada secara default. Satu perintah menyiapkan semuanya:
php artisan install:api
php artisan migrate
Perintah pertama memasang laravel/sanctum, membuat routes/api.php, mendaftarkannya di bootstrap/app.php, dan menerbitkan migration tabel token. Pastikan koneksi database di .env sudah benar sebelum migrate; panduan konfigurasi .env database membahasnya.
Tambahkan trait HasApiTokens ke model User. Langkah ini sering terlupa karena tidak dilakukan otomatis:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}
Langkah 2: Register, Login, Logout
php artisan make:controller Api/AuthController
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function register(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
'device_name' => 'required|string|max:100',
]);
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
$token = $user->createToken($data['device_name'], ['product:read'])->plainTextToken;
return response()->json(['user' => $user, 'token' => $token], 201);
}
public function login(Request $request)
{
$data = $request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required|string|max:100',
]);
$user = User::where('email', $data['email'])->first();
if (! $user || ! Hash::check($data['password'], $user->password)) {
return response()->json(['message' => 'Kredensial tidak valid.'], 401);
}
$abilities = $user->is_admin ? ['*'] : ['product:read'];
$token = $user->createToken($data['device_name'], $abilities, now()->addDays(30));
return response()->json(['token' => $token->plainTextToken]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Berhasil logout.']);
}
}
Beberapa keputusan di kode ini patut dijelaskan:
- Pesan error login sama untuk email tidak terdaftar maupun password salah, supaya penyerang tidak bisa menebak email mana yang valid.
- device_name menjadi nama token. Saat user kehilangan HP, Anda cukup menghapus token bernama "HP Samsung" tanpa memaksa logout di perangkat lain.
- Parameter ketiga
createToken()menetapkan tanggal kedaluwarsa per token. Kolomis_admindi contoh hanyalah asumsi; sesuaikan dengan cara aplikasi Anda menandai admin. - Token disimpan sebagai hash SHA-256 di database. Teks aslinya hanya muncul sekali di respons, jadi klien wajib menyimpannya.
Langkah 3: Route dan Proteksi
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\ProductController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:5,1');
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:5,1');
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $request) => $request->user());
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/products', [ProductController::class, 'index']);
Route::post('/products', [ProductController::class, 'store'])->middleware('abilities:product:write');
});
throttle:5,1 membatasi lima percobaan per menit per IP, cukup untuk manusia tetapi menyulitkan brute force. Middleware abilities perlu didaftarkan dulu di bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
]);
})
Di dalam controller Anda juga bisa memeriksa secara manual dengan $request->user()->tokenCan('product:write').
Langkah 4: JSON Rapi dengan API Resource
Mengembalikan model mentah berarti setiap kolom baru di tabel ikut terkirim ke klien, termasuk yang tidak seharusnya. API Resource memisahkan struktur database dari kontrak API:
php artisan make:resource ProductResource
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => (int) $this->price,
'created_at' => $this->created_at->toDateString(),
];
}
public function index()
{
return ProductResource::collection(Product::latest()->paginate(10));
}
Koleksi berpaginasi otomatis membawa blok links dan meta, sehingga aplikasi mobile tahu ada halaman berikutnya atau tidak. Untuk endpoint daftar yang memuat relasi, pakai eager loading agar tidak terjadi N+1 query; detailnya ada di artikel optimasi query Eloquent.
Langkah 5: Error Selalu dalam JSON
Klien yang lupa mengirim Accept: application/json akan menerima halaman HTML atau redirect ke /login saat token salah. Paksa semua route api/* merespons JSON di bootstrap/app.php:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->shouldRenderJsonWhen(
fn (Request $request, Throwable $e) => $request->is('api/*') || $request->expectsJson()
);
})
Langkah 6: Uji dengan cURL
curl -X POST http://localhost:8000/api/login \
-H "Accept: application/json" \
-d "email=budi@mail.com&password=rahasia88&device_name=postman"
curl http://localhost:8000/api/user \
-H "Accept: application/json" \
-H "Authorization: Bearer 1|xxxxxxxxxxxxxxxx"
Di Postman, pilih tab Authorization, tipe Bearer Token, lalu tempel token tersebut.
Langkah 7: Kunci Perilaku dengan Feature Test
Mengetes API secara manual lewat Postman setiap kali ada perubahan cepat membosankan, dan akhirnya dilewati. Feature test menjalankan skenario yang sama dalam hitungan detik. Helper Sanctum::actingAs() membuat user terautentikasi dengan abilities tertentu tanpa perlu login sungguhan:
use App\Models\User;
use Laravel\Sanctum\Sanctum;
public function test_tamu_tidak_bisa_melihat_produk(): void
{
$this->getJson('/api/products')->assertUnauthorized();
}
public function test_token_read_tidak_bisa_menambah_produk(): void
{
Sanctum::actingAs(User::factory()->create(), ['product:read']);
$this->postJson('/api/products', ['name' => 'Kopi'])->assertForbidden();
}
Dua test ini menjaga hal yang paling mahal jika rusak: route yang tiba-tiba terbuka tanpa token, dan token biasa yang bisa menulis data.
Merencanakan Versi API
Begitu aplikasi mobile terpasang di ribuan HP, Anda tidak bisa memaksa semua orang memperbarui aplikasi di hari yang sama. Karena itu, beri prefix versi sejak awal, misalnya Route::prefix('v1'). Saat format respons harus berubah drastis, buat v2 dan biarkan v1 hidup beberapa bulan sambil memantau pemakaiannya di log. Perubahan kecil seperti menambah field baru tidak memerlukan versi baru karena klien lama cukup mengabaikannya.
Error Umum dan Solusinya
| Gejala | Penyebab | Perbaikan |
|---|---|---|
Call to undefined method createToken() | Trait HasApiTokens belum dipasang | Tambahkan use HasApiTokens di model User |
| Error "Route [login] not defined" | Request tanpa header Accept dialihkan ke halaman login | Kirim header Accept atau pakai shouldRenderJsonWhen |
404 pada /api/... | install:api belum dijalankan | Jalankan perintahnya, lalu cek php artisan route:list |
| Selalu 401 padahal token benar | Header Authorization dibuang oleh server (umum di Apache/CGI) | Pastikan .htaccess bawaan Laravel di folder public terpasang |
| Tabel token membengkak | Token kedaluwarsa tidak pernah dihapus | Jadwalkan sanctum:prune-expired |
Untuk poin terakhir, tambahkan jadwal di routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('sanctum:prune-expired --hours=24')->daily();
Checklist Sebelum API Dipakai Produksi
- Seluruh trafik lewat HTTPS, tanpa pengecualian.
- Endpoint login dan register diberi throttle.
- Token diberi nama perangkat dan masa berlaku.
- Abilities dibatasi seminimal mungkin per peran.
- Semua respons memakai API Resource, bukan model mentah.
- Error 401, 403, 404, dan 422 selalu berbentuk JSON.
- Ada feature test untuk login gagal, login sukses, dan akses tanpa token.
Daftar praktik keamanan yang lebih luas ada di 10 best practice keamanan aplikasi web. Dengan pola di atas, API Anda tidak hanya berjalan, tetapi juga mudah dirawat ketika jumlah klien dan endpoint bertambah.