API Development

Building RESTful APIs with Laravel 12: Best Practices and Tips

Learn how to build secure, scalable, and RESTful APIs using Laravel 12. This guide covers structure, authentication, versioning, and performance optimization.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
02-Nov-2025
7 min read
Building RESTful APIs with Laravel 12: Best Practices and Tips

APIs power modern web and mobile applications, enabling seamless communication between different systems. Laravel 12 provides an elegant and robust toolkit for building RESTful APIs that are secure, maintainable, and performant. This comprehensive guide covers essential best practices to help you create production-ready APIs that scale.

Understanding RESTful API Design

Before diving into Laravel-specific implementations, let's establish REST principles:

  • Stateless communication – Each request contains all necessary information
  • Resource-based URLs – Use nouns, not verbs (e.g., /users, not /getUsers)
  • HTTP methods – GET (read), POST (create), PUT/PATCH (update), DELETE (remove)
  • Standard status codes – 200 (OK), 201 (Created), 404 (Not Found), 422 (Validation Error)
  • JSON responses – Consistent data format across endpoints

Setting Up API Routes in Laravel 12

Laravel provides a dedicated routes/api.php file for API routes. These routes are automatically prefixed with /api and are stateless by default.

Basic Resource Routes

use App\Http\Controllers\Api\UserController;
use Illuminate\Support\Facades\Route;

// RESTful resource routes
Route::apiResource('users', UserController::class);

// Manual route definitions
Route::get('/users', [UserController::class, 'index']);
Route::post('/users', [UserController::class, 'store']);
Route::get('/users/{user}', [UserController::class, 'show']);
Route::put('/users/{user}', [UserController::class, 'update']);
Route::patch('/users/{user}', [UserController::class, 'update']);
Route::delete('/users/{user}', [UserController::class, 'destroy']);

Route Model Binding

Laravel 12's route model binding automatically resolves Eloquent models:

Route::get('/users/{user}', function (User $user) {
    return new UserResource($user);
});

Creating API Controllers

Generate API-specific controllers with the --api flag:

php artisan make:controller Api/UserController --api --model=User

Controller Best Practices

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class UserController extends Controller
{
    /**
     * Display a listing of users.
     */
    public function index(): AnonymousResourceCollection
    {
        $users = User::query()
            ->with('profile')
            ->paginate(15);
            
        return UserResource::collection($users);
    }

    /**
     * Store a newly created user.
     */
    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ]);

        $validated['password'] = bcrypt($validated['password']);
        $user = User::create($validated);

        return (new UserResource($user))
            ->response()
            ->setStatusCode(201);
    }

    /**
     * Display the specified user.
     */
    public function show(User $user): UserResource
    {
        return new UserResource($user->load('profile', 'posts'));
    }

    /**
     * Update the specified user.
     */
    public function update(Request $request, User $user): UserResource
    {
        $validated = $request->validate([
            'name' => 'sometimes|string|max:255',
            'email' => 'sometimes|email|unique:users,email,' . $user->id,
        ]);

        $user->update($validated);

        return new UserResource($user);
    }

    /**
     * Remove the specified user.
     */
    public function destroy(User $user): JsonResponse
    {
        $user->delete();

        return response()->json([
            'message' => 'User deleted successfully'
        ], 204);
    }
}

Using API Resources for Consistent Responses

API Resources transform your Eloquent models into JSON responses with complete control over data structure.

Creating Resources

php artisan make:resource UserResource
php artisan make:resource UserCollection

Building Resource Classes

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
            
            // Conditional attributes
            'email_verified' => $this->when(
                $request->user()?->isAdmin(),
                $this->email_verified_at !== null
            ),
            
            // Relationship loading
            'profile' => new ProfileResource($this->whenLoaded('profile')),
            'posts' => PostResource::collection($this->whenLoaded('posts')),
            
            // Computed values
            'posts_count' => $this->when(
                $this->relationLoaded('posts'),
                fn() => $this->posts->count()
            ),
        ];
    }

    /**
     * Get additional data that should be returned with the resource array.
     */
    public function with(Request $request): array
    {
        return [
            'meta' => [
                'version' => '1.0',
                'timestamp' => now()->toISOString(),
            ],
        ];
    }
}

Using Resources in Controllers

// Single resource
return new UserResource($user);

// Collection
return UserResource::collection(User::paginate(15));

// With additional metadata
return UserResource::collection($users)->additional([
    'meta' => [
        'total_active' => User::where('active', true)->count(),
    ],
]);

Implementing Authentication with Laravel Sanctum

Laravel 12 uses Sanctum for API token authentication, perfect for SPAs and mobile applications.

Installation and Setup

composer require laravel/sanctum
php artisan install:api

This command publishes configuration and runs migrations. Update your User model:

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    
    // ... rest of model
}

Token Generation

namespace App\Http\Controllers\Api;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required',
            'device_name' => '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.'],
            ]);
        }

        $token = $user->createToken($request->device_name)->plainTextToken;

        return response()->json([
            'user' => new UserResource($user),
            'token' => $token,
            'token_type' => 'Bearer',
        ]);
    }

    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();

        return response()->json([
            'message' => 'Logged out successfully'
        ]);
    }
}

Protecting Routes

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::apiResource('posts', PostController::class);
    Route::post('/logout', [AuthController::class, 'logout']);
});

API Versioning Strategy

Versioning ensures backward compatibility when making breaking changes to your API.

URL-Based Versioning

// routes/api.php
Route::prefix('v1')->name('v1.')->group(function () {
    Route::apiResource('users', Api\V1\UserController::class);
    Route::apiResource('posts', Api\V1\PostController::class);
});

Route::prefix('v2')->name('v2.')->group(function () {
    Route::apiResource('users', Api\V2\UserController::class);
    Route::apiResource('posts', Api\V2\PostController::class);
});

Directory Structure

app/Http/Controllers/Api/
├── V1/
│   ├── UserController.php
│   └── PostController.php
└── V2/
    ├── UserController.php
    └── PostController.php

Error Handling and Validation

Custom Exception Handler

Update bootstrap/app.php in Laravel 12:

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

return Application::configure(basePath: dirname(__DIR__))
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->render(function (NotFoundHttpException $e, Request $request) {
            if ($request->is('api/*')) {
                return response()->json([
                    'status' => 'error',
                    'message' => 'Resource not found',
                ], 404);
            }
        });
    })->create();

Consistent Response Format

namespace App\Traits;

trait ApiResponse
{
    protected function successResponse($data, string $message = 'Success', int $code = 200)
    {
        return response()->json([
            'status' => 'success',
            'message' => $message,
            'data' => $data,
        ], $code);
    }

    protected function errorResponse(string $message, int $code = 400, ?array $errors = null)
    {
        return response()->json([
            'status' => 'error',
            'message' => $message,
            'errors' => $errors,
        ], $code);
    }
}

Rate Limiting

Laravel 12 provides built-in rate limiting for API protection:

// bootstrap/app.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

// Custom rate limits
RateLimiter::for('uploads', function (Request $request) {
    return $request->user()?->isPremium()
        ? Limit::none()
        : Limit::perMinute(10);
});

Apply rate limiting to routes:

Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::apiResource('posts', PostController::class);
});

Performance Optimization

Eager Loading Relationships

public function index()
{
    $users = User::with(['profile', 'posts:id,user_id,title'])
        ->withCount('posts')
        ->paginate(15);
        
    return UserResource::collection($users);
}

Caching Strategies

use Illuminate\Support\Facades\Cache;

public function index()
{
    $users = Cache::remember('users.all', 3600, function () {
        return User::with('profile')->get();
    });
    
    return UserResource::collection($users);
}

Pagination Best Practices

// Standard pagination
$users = User::paginate(15);

// Cursor pagination for better performance
$users = User::cursorPaginate(15);

// Custom pagination parameters
$perPage = $request->input('per_page', 15);
$users = User::paginate(min($perPage, 100));

Testing Your API

namespace Tests\Feature\Api;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_list_users()
    {
        User::factory()->count(5)->create();

        $response = $this->getJson('/api/v1/users');

        $response->assertStatus(200)
            ->assertJsonStructure([
                'data' => [
                    '*' => ['id', 'name', 'email']
                ]
            ]);
    }

    public function test_can_create_user_with_authentication()
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user, 'sanctum')
            ->postJson('/api/v1/users', [
                'name' => 'John Doe',
                'email' => 'john@example.com',
            ]);

        $response->assertStatus(201)
            ->assertJson([
                'data' => [
                    'name' => 'John Doe',
                    'email' => 'john@example.com',
                ]
            ]);
    }
}

Conclusion

Building RESTful APIs with Laravel 12 combines elegant syntax with powerful features. By implementing API resources for consistent responses, Sanctum for authentication, proper versioning strategies, comprehensive error handling, rate limiting, and performance optimizations, you'll create APIs that are secure, maintainable, and scale effortlessly.

Remember to always validate input data, use proper HTTP status codes, document your API endpoints, and write comprehensive tests. These practices ensure your API remains reliable and developer-friendly as your application grows.


Article Tags

Laravel 12 RESTful API Laravel API API Development Laravel Sanctum API Authentication API Resources Laravel Best Practices Web Services API Versioning Laravel Performance Rate Limiting JSON API Backend Development PHP API Development

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products