Building robust REST APIs is one of the most in-demand Laravel skills in 2026. Getting authentication right, returning consistent responses, and following RESTful conventions from day one saves enormous pain when your API grows.

This guide covers everything from project setup to production-grade authentication — the same patterns I use across 10+ production APIs.

1. Project Setup & Configuration

bash
# Create new Laravel project
composer create-project laravel/laravel api-project
cd api-project

# Install Sanctum for API authentication
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
php
// config/app.php — always use UTC for APIs
'timezone' => 'UTC',

// .env
API_VERSION=v1
API_RATE_LIMIT=60

2. API Routing & Versioning

Always version your API from day one. Changing /api/products to /api/v2/products later breaks every client. Starting with /api/v1/ costs nothing upfront.

php
// routes/api.php
use App\Http\Controllers\Api\V1\AuthController;
use App\Http\Controllers\Api\V1\UserController;
use App\Http\Controllers\Api\V1\ProductController;

Route::prefix('v1')->group(function () {
    // Public routes — no auth required
    Route::post('/register', [AuthController::class, 'register']);
    Route::post('/login',    [AuthController::class, 'login']);

    // Protected routes — Sanctum auth required
    Route::middleware(['auth:sanctum'])->group(function () {
        Route::post('/logout',   [AuthController::class, 'logout']);
        Route::get('/profile',   [UserController::class, 'profile']);
        Route::apiResource('products', ProductController::class);
    });
});

3. Authentication with Sanctum

Laravel Sanctum is the right choice for most REST APIs — it's lightweight, official, and handles token-based authentication cleanly. Here's the full implementation:

php
// app/Http/Controllers/Api/V1/AuthController.php
class AuthController extends Controller
{
    public function register(RegisterRequest $request)
    {
        $user = User::create([
            'name'     => $request->name,
            'email'    => $request->email,
            'password' => Hash::make($request->password),
        ]);

        $token = $user->createToken('auth_token')->plainTextToken;

        return $this->successResponse([
            'user'       => $user,
            'token'      => $token,
            'token_type' => 'Bearer',
        ], 'User registered successfully', 201);
    }

    public function login(LoginRequest $request)
    {
        if (!Auth::attempt($request->only('email', 'password'))) {
            return $this->errorResponse('Invalid credentials', 401);
        }

        $user  = Auth::user();
        $token = $user->createToken('auth_token')->plainTextToken;

        return $this->successResponse([
            'user'       => $user,
            'token'      => $token,
            'token_type' => 'Bearer',
        ], 'Login successful');
    }

    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();
        return $this->successResponse(null, 'Logged out successfully');
    }
}

How the client uses the token: After login, store the token and send it on every request as Authorization: Bearer {token}. Laravel's auth:sanctum middleware validates it automatically.

bash
# Example request with Bearer token
curl -X GET https://yourapi.com/api/v1/profile \
  -H "Authorization: Bearer 1|abc123yourtoken" \
  -H "Accept: application/json"

4. Request Validation

Always use Form Requests for validation — never validate inline in controllers. It keeps controllers clean and makes validation reusable.

php
// app/Http/Requests/RegisterRequest.php
class RegisterRequest extends FormRequest
{
    public function authorize(): bool { return true; }

    public function rules(): array
    {
        return [
            'name'     => 'required|string|max:255',
            'email'    => 'required|email|unique:users,email',
            'password' => 'required|string|min:8|confirmed',
        ];
    }

    public function messages(): array
    {
        return [
            'email.unique'       => 'This email is already registered.',
            'password.confirmed' => 'Password confirmation does not match.',
        ];
    }
}

5. Consistent Error Handling

Nothing frustrates API consumers more than inconsistent response formats. Build a base controller with standard response methods used everywhere:

php
// app/Http/Controllers/Controller.php
class Controller extends BaseController
{
    protected function successResponse($data, string $message = 'Success', int $code = 200)
    {
        return response()->json([
            'success' => true,
            'message' => $message,
            'data'    => $data,
        ], $code);
    }

    protected function errorResponse(string $message, int $code = 400, $errors = null)
    {
        return response()->json(array_filter([
            'success' => false,
            'message' => $message,
            'errors'  => $errors,
        ]), $code);
    }
}
php
// app/Exceptions/Handler.php — global JSON error handling
public function render($request, Throwable $exception)
{
    if ($request->expectsJson()) {
        if ($exception instanceof ModelNotFoundException) {
            return response()->json(['success' => false, 'message' => 'Resource not found'], 404);
        }
        if ($exception instanceof AuthenticationException) {
            return response()->json(['success' => false, 'message' => 'Unauthenticated'], 401);
        }
        if ($exception instanceof ValidationException) {
            return response()->json([
                'success' => false,
                'message' => 'Validation failed',
                'errors'  => $exception->errors(),
            ], 422);
        }
    }
    return parent::render($request, $exception);
}

6. API Documentation

Use Scribe for simple auto-generated docs, or L5-Swagger for full OpenAPI/Swagger support. Always include a Postman collection for your API consumers — it removes the biggest barrier to adoption.

7. Testing Your API

php
// tests/Feature/AuthTest.php
class AuthTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_register(): void
    {
        $response = $this->postJson('/api/v1/register', [
            'name'                  => 'John Doe',
            'email'                 => 'john@example.com',
            'password'              => 'password123',
            'password_confirmation' => 'password123',
        ]);

        $response->assertStatus(201)
                 ->assertJsonStructure([
                     'success', 'message',
                     'data' => ['user', 'token', 'token_type']
                 ]);

        $this->assertDatabaseHas('users', ['email' => 'john@example.com']);
    }

    public function test_user_can_login(): void
    {
        $user = User::factory()->create(['password' => Hash::make('password123')]);

        $response = $this->postJson('/api/v1/login', [
            'email'    => $user->email,
            'password' => 'password123',
        ]);

        $response->assertStatus(200)
                 ->assertJsonPath('success', true)
                 ->assertJsonStructure(['data' => ['token']]);
    }
}

8. Performance Tips

The five highest-impact performance improvements for Laravel APIs: eager loading to eliminate N+1 queries, Redis caching for frequently accessed data, rate limiting to protect against abuse, API Resources for consistent data transformation, and offloading heavy operations to queue jobs.

php
// Add rate limiting to routes/api.php
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
    Route::apiResource('products', ProductController::class);
});

// Use API Resources for consistent response format
php artisan make:resource ProductResource

// Eager load to prevent N+1 — always check with Laravel Debugbar
$products = Product::with(['category', 'tags'])->paginate(15);

9. Conclusion

The foundation of a good Laravel REST API is consistent authentication with Sanctum, versioned routes, Form Request validation, and standardised JSON responses. Build these in from day one — retrofitting them into a large codebase later is far more painful than getting them right upfront.

For the full authentication comparison, see Does Laravel Sanctum Use JWT? Sanctum vs JWT Explained. For handling background tasks in your API, check the Laravel Queues & Jobs guide.

Work with me

Need help with your Laravel architecture?

I build clean, maintainable Laravel backends for SaaS products — REST APIs, queue systems and production patterns like the one in this article.