REST API Development
LaravelBuilding a production-ready REST API in Laravel involves proper resource design, validation, error handling, versioning, and pagination.
API Resource Classes
Resources transform your models into JSON responses with full control over what gets returned:
Terminal
php artisan make:resource PostResource
php artisan make:resource PostCollectionapp/Http/Resources/PostResource.php
<?php
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => Str::limit($this->body, 200),
'body' => $this->when($request->routeIs('posts.show'), $this->body),
'published' => $this->published,
'views' => $this->views,
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
'author' => new UserResource($this->whenLoaded('user')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'links' => [
'self' => route('api.posts.show', $this->id),
],
];
}
}Form Request Validation
Terminal + PHP
php artisan make:request StorePostRequestapp/Http/Requests/StorePostRequest.php
<?php
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->check(); // Must be logged in
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string|min:10',
'published' => 'boolean',
'tags' => 'array',
'tags.*' => 'integer|exists:tags,id',
];
}
public function messages(): array
{
return [
'title.required' => 'A post title is required.',
'body.min' => 'Post body must be at least 10 characters.',
];
}
}Full CRUD API Controller
app/Http/Controllers/Api/PostController.php
<?php
class PostController extends Controller
{
public function index(Request $request)
{
$posts = Post::query()
->with(['user', 'tags'])
->when($request->search, fn($q) => $q->where('title', 'like', "%{$request->search}%"))
->when($request->published, fn($q) => $q->where('published', true))
->latest()
->paginate($request->per_page ?? 15);
return PostResource::collection($posts);
}
public function store(StorePostRequest $request)
{
$post = Post::create([
...$request->validated(),
'user_id' => auth()->id(),
'slug' => Str::slug($request->title),
]);
if ($request->tags) {
$post->tags()->sync($request->tags);
}
return new PostResource($post->load('user', 'tags'));
}
public function show(Post $post)
{
$post->increment('views');
return new PostResource($post->load('user', 'tags'));
}
public function update(UpdatePostRequest $request, Post $post)
{
$this->authorize('update', $post); // Policy check
$post->update($request->validated());
return new PostResource($post);
}
public function destroy(Post $post)
{
$this->authorize('delete', $post);
$post->delete();
return response()->json(['message' => 'Post deleted'], 200);
}
}API Versioning
routes/api.php
<?php
// Version 1
Route::prefix('v1')->group(function () {
Route::apiResource('posts', V1\PostController::class);
});
// Version 2 (new features, breaking changes)
Route::prefix('v2')->group(function () {
Route::apiResource('posts', V2\PostController::class);
});
// URLs: /api/v1/posts, /api/v2/postsGlobal Error Handling
app/Exceptions/Handler.php
<?php
public function register(): void
{
$this->renderable(function (ModelNotFoundException $e) {
return response()->json(['error' => 'Resource not found'], 404);
});
$this->renderable(function (AuthenticationException $e) {
return response()->json(['error' => 'Unauthenticated'], 401);
});
$this->renderable(function (ValidationException $e) {
return response()->json([
'error' => 'Validation failed',
'details' => $e->errors(),
], 422);
});
}💡
Always return consistent JSON! Use the same structure for all responses:
{data: ..., message: ..., errors: ...}. This makes it easier for frontend developers to handle your API.Test your knowledge!
Take the Laravel quiz.