Laravel makes building REST APIs surprisingly straightforward. In this guide we’ll build a complete, production-ready API from scratch using Laravel 12 — covering everything from setup to authentication.
By the end you’ll have a fully working API that follows REST best practices and is ready to connect to any frontend — React, Vue, mobile app, or third-party service.
What We’re Building
- Full CRUD operations (Create, Read, Update, Delete)
- API authentication using Laravel Sanctum
- API Resources for consistent response formatting
- Request validation
- Proper HTTP status codes and error handling
Prerequisites
- PHP 8.2+
- Composer installed
- Basic understanding of PHP and MVC concepts
- Postman or Hoppscotch for testing
Step 1 — Create a New Laravel 12 Project
composer create-project laravel/laravel taskapi
cd taskapi
Set up your database in .env:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=taskapi
DB_USERNAME=root
DB_PASSWORD=
Step 2 — Create the Task Model and Migration
php artisan make:model Task -m
Open the migration file and define the schema:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('description')->nullable();
$table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
$table->timestamp('due_date')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};
php artisan migrate
Step 3 — Set Up the Task Model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Task extends Model
{
protected $fillable = [
'title',
'description',
'status',
'due_date',
'user_id',
];
protected $casts = [
'due_date' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Step 4 — Install Laravel Sanctum
Laravel 12 includes Sanctum by default. Run:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Add HasApiTokens to your User model:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
}
Step 5 — Create an API Resource
php artisan make:resource TaskResource
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'status' => $this->status,
'due_date' => $this->due_date?->toDateString(),
'created_at' => $this->created_at->toDateTimeString(),
];
}
}
Step 6 — Create the Task Controller
php artisan make:controller Api/TaskController --api
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\TaskResource;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class TaskController extends Controller
{
public function index(Request $request)
{
$tasks = $request->user()->tasks()->latest()->paginate(10);
return TaskResource::collection($tasks);
}
public function store(Request $request): TaskResource
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'in:pending,in_progress,completed',
'due_date' => 'nullable|date',
]);
$task = $request->user()->tasks()->create($validated);
return new TaskResource($task);
}
public function show(Request $request, Task $task): TaskResource
{
$this->authorize('view', $task);
return new TaskResource($task);
}
public function update(Request $request, Task $task): TaskResource
{
$this->authorize('update', $task);
$validated = $request->validate([
'title' => 'sometimes|string|max:255',
'description' => 'nullable|string',
'status' => 'in:pending,in_progress,completed',
'due_date' => 'nullable|date',
]);
$task->update($validated);
return new TaskResource($task);
}
public function destroy(Request $request, Task $task): JsonResponse
{
$this->authorize('delete', $task);
$task->delete();
return response()->json(['message' => 'Task deleted successfully']);
}
}
Step 7 — Create Auth Controller
php artisan make:controller Api/AuthController
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function register(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
$token = $user->createToken('api-token')->plainTextToken;
return response()->json(['user' => $user, 'token' => $token], 201);
}
public function login(Request $request): JsonResponse
{
$request->validate([
'email' => 'required|email',
'password' => '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('api-token')->plainTextToken;
return response()->json(['user' => $user, 'token' => $token]);
}
public function logout(Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out successfully']);
}
}
Step 8 — Define API Routes
<?php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\TaskController;
use Illuminate\Support\Facades\Route;
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::apiResource('tasks', TaskController::class);
});
Step 9 — Test the API
php artisan serve
Register a user:
POST http://localhost:8000/api/register
Content-Type: application/json
{
"name": "Atul",
"email": "atul@example.com",
"password": "password123",
"password_confirmation": "password123"
}
Create a task:
POST http://localhost:8000/api/tasks
Authorization: Bearer YOUR_TOKEN_HERE
Content-Type: application/json
{
"title": "Build the API",
"description": "Complete the Laravel REST API guide",
"status": "in_progress",
"due_date": "2026-06-30"
}
API Endpoints Summary
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/register | Register new user | No |
| POST | /api/login | Login user | No |
| POST | /api/logout | Logout user | Yes |
| GET | /api/tasks | List all tasks | Yes |
| POST | /api/tasks | Create task | Yes |
| GET | /api/tasks/{id} | Get single task | Yes |
| PUT | /api/tasks/{id} | Update task | Yes |
| DELETE | /api/tasks/{id} | Delete task | Yes |
Best Practices for Production
Version your API: Use /api/v1/ prefix so you can release v2 without breaking existing clients.
Rate limiting:
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
// your routes
});
Cache repeated responses:
$tasks = cache()->remember('user_tasks_' . $request->user()->id, 300, function () use ($request) {
return $request->user()->tasks()->latest()->paginate(10);
});
If you need a custom REST API built for your business — whether it’s a mobile app backend, third-party integration, or internal tool — our team at Softcrony is happy to help.
Leave a comment