Laravel
API Development
Build robust REST APIs using Laravel Sanctum for token authentication, API Resources for consistent JSON responses, and versioned route prefixes.
Laravel Sanctum Install & Setup: Sanctum provides a simple token-based authentication system for SPAs and mobile apps. Install it via Composer, publish the config, and run migrations.
terminal
BASH
# 1. Install Sanctum
composer require laravel/sanctum
# 2. Publish config and migration
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
# 3. Run migrations (creates personal_access_tokens table)
php artisan migrate
# 4. Generate API controller
php artisan make:controller Api/V1/PostController --api --model=Post
# 5. Generate API Resource and Collection
php artisan make:resource PostResource
php artisan make:resource PostCollection
Sanctum Model & Middleware Setup: Add the
HasApiTokens trait to the User model. Ensure Sanctum's middleware is registered for API routes.
app/Models/User.php
PHP
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
// bootstrap/app.php (Laravel 11) – ensure sanctum middleware is available
// return Application::configure(basePath: dirname(__DIR__))
// ->withMiddleware(function (Middleware $middleware) {
// $middleware->statefulApi(); // Sanctum stateful domains
// })
// ->create();
// config/sanctum.php – token expiration
// 'expiration' => 60 * 24 * 7, // 7 days in minutes
// 'token_prefix' => '',
API Routes with Versioning: Organise routes in
routes/api.php using version prefixes and auth:sanctum middleware to protect endpoints.
routes/api.php
PHP
<?php
use App\Http\Controllers\Api\V1\AuthController;
use App\Http\Controllers\Api\V1\PostController;
use App\Http\Controllers\Api\V1\UserController;
use Illuminate\Support\Facades\Route;
// ── Public routes (no auth required) ────────────────────────────────────────
Route::prefix('v1')->group(function () {
// Authentication
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Public resource endpoints
Route::apiResource('posts', PostController::class)->only(['index', 'show']);
});
// ── Protected routes (requires valid Sanctum token) ─────────────────────────
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
// Auth actions
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', [UserController::class, 'me']);
Route::put('/user', [UserController::class, 'update']);
Route::post('/user/avatar', [UserController::class, 'updateAvatar']);
// Authenticated resource (excludes public index/show – already registered)
Route::apiResource('posts', PostController::class)->except(['index', 'show']);
// Nested resource
Route::apiResource('posts.comments', \App\Http\Controllers\Api\V1\CommentController::class)
->shallow();
});
// ── Version 2 routes ─────────────────────────────────────────────────────────
Route::prefix('v2')->middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', \App\Http\Controllers\Api\V2\PostController::class);
});
API Auth Controller: Handle token issuance on login, registration, and token revocation on logout. Always return structured JSON using helper methods.
app/Http/Controllers/Api/V1/AuthController.php
PHP
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function register(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
$user = User::create($validated);
$token = $user->createToken('api-token')->plainTextToken;
return response()->json([
'message' => 'Registration successful.',
'user' => $user,
'token' => $token,
], 201);
}
public function login(Request $request): JsonResponse
{
$request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
// Optionally revoke old tokens
$user->tokens()->delete();
// Create token with abilities (scopes)
$token = $user->createToken('api-token', ['posts:read', 'posts:write'])
->plainTextToken;
return response()->json([
'message' => 'Login successful.',
'user' => $user,
'token' => $token,
]);
}
public function logout(Request $request): JsonResponse
{
// Revoke the current token
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out successfully.']);
}
}
API Resource Class: Transform Eloquent models into consistent JSON structures using
JsonResource. Use ResourceCollection for paginated lists with metadata.
app/Http/Resources/PostResource.php
PHP
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'type' => 'post',
'attributes' => [
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'body' => $this->when(
$request->routeIs('posts.show'),
$this->body // only include full body on single-post endpoint
),
'status' => $this->status,
'image_url' => $this->image
? asset('storage/' . $this->image)
: null,
'published_at' => $this->published_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
],
'relationships' => [
'author' => new UserResource($this->whenLoaded('user')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
],
'links' => [
'self' => route('posts.show', $this->id),
],
];
}
// Wrap collection with extra metadata
public static function collection($resource)
{
return parent::collection($resource)->additional([
'meta' => ['api_version' => 'v1'],
]);
}
}
API Resource Controller: A clean API controller that returns proper HTTP status codes, uses API Resources for output, and handles not-found cases gracefully.
app/Http/Controllers/Api/V1/PostController.php
PHP
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class PostController extends Controller
{
// GET /api/v1/posts
public function index(Request $request): AnonymousResourceCollection
{
$posts = Post::query()
->with(['user', 'tags'])
->when($request->search, fn($q, $s) => $q->where('title', 'like', "%$s%"))
->when($request->status, fn($q, $s) => $q->where('status', $s))
->latest()
->paginate($request->per_page ?? 15);
return PostResource::collection($posts);
}
// GET /api/v1/posts/{post}
public function show(Post $post): PostResource
{
$post->loadMissing(['user', 'tags', 'comments.user']);
return new PostResource($post);
}
// POST /api/v1/posts
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
'status' => ['required', 'in:draft,published'],
'tags' => ['nullable', 'array'],
'tags.*' => ['exists:tags,id'],
]);
$post = $request->user()->posts()->create($validated);
$post->tags()->sync($validated['tags'] ?? []);
return (new PostResource($post))
->response()
->setStatusCode(201);
}
// PUT /api/v1/posts/{post}
public function update(Request $request, Post $post): PostResource
{
$this->authorize('update', $post); // Policy check
$validated = $request->validate([
'title' => ['sometimes', 'string', 'max:255'],
'body' => ['sometimes', 'string'],
'status' => ['sometimes', 'in:draft,published'],
]);
$post->update($validated);
return new PostResource($post->fresh());
}
// DELETE /api/v1/posts/{post}
public function destroy(Post $post): JsonResponse
{
$this->authorize('delete', $post);
$post->delete();
return response()->json(['message' => 'Post deleted.'], 200);
}
}
Returning JSON Responses & Error Handling: Use
response()->json() for custom responses. Register a global exception handler in bootstrap/app.php to always return JSON for API requests.
bootstrap/app.php (Laravel 11) + json-response-helpers.php
PHP
// ── Common JSON response patterns ────────────────────────────────────────────
// Success with data
return response()->json([
'success' => true,
'data' => $data,
'message' => 'Operation completed.',
], 200);
// Created
return response()->json([
'success' => true,
'data' => new PostResource($post),
], 201);
// No content
return response()->json(null, 204);
// Client error
return response()->json([
'success' => false,
'message' => 'Resource not found.',
'errors' => [],
], 404);
// ── Global API exception handler (bootstrap/app.php Laravel 11) ──────────────
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
return Application::configure(basePath: dirname(__DIR__))
->withExceptions(function (Exceptions $exceptions) {
// Always return JSON for API routes
$exceptions->shouldRenderJsonWhen(
fn(Request $request) => $request->is('api/*')
);
// Custom 404 response
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'success' => false,
'message' => 'Resource not found.',
], 404);
}
});
// Validation exception formatting
$exceptions->render(function (ValidationException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'success' => false,
'message' => 'Validation failed.',
'errors' => $e->errors(),
], 422);
}
});
})
->create();
Token Abilities (Sanctum Scopes): Assign fine-grained abilities to tokens and check them in controllers or middleware to restrict what each token can do.
token-abilities-example.php
PHP
// Issue token with specific abilities (scopes)
$token = $user->createToken('mobile-app', [
'posts:read',
'posts:write',
'profile:update',
])->plainTextToken;
// Check ability in a controller
public function store(Request $request): JsonResponse
{
if (! $request->user()->tokenCan('posts:write')) {
return response()->json([
'message' => 'Token does not have permission to create posts.',
], 403);
}
// ... proceed
}
// Middleware shorthand in routes
Route::middleware(['auth:sanctum', 'abilities:posts:write'])->group(function () {
Route::post('/posts', [PostController::class, 'store']);
});
// Check ANY of multiple abilities
Route::middleware(['auth:sanctum', 'ability:posts:read,posts:write'])->group(function () {
Route::get('/posts', [PostController::class, 'index']);
});
// Revoke specific tokens
$user->tokens()->where('name', 'mobile-app')->delete();
// Revoke all tokens (e.g. on password change)
$user->tokens()->delete();