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
composer create-project laravel/laravel api-project
cd api-project
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
'timezone' => 'UTC',
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.
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 () {
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
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:
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.
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.
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:
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);
}
}
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
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']]);
}
}
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.
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::apiResource('products', ProductController::class);
});
php artisan make:resource ProductResource
$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.