Lessons → Laravel Advanced → Lesson 2

REST API Development

Laravel
⏱ 30 min read🔥 AdvancedNot completed

Building 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 PostCollection
app/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 StorePostRequest
app/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/posts

Global 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.
← Previous Lesson Next Lesson →
🧠

Test your knowledge!

Take the Laravel quiz.

Take Quiz →