🏠 Home / Hub

🧱 Projects 03 — Laravel API Backend

← Projects Menu

Goal: Vue Dashboard (Project 02) နဲ့ Flutter Mobile (Project 04) တို့ connect လုပ်မယ့် REST API တစ်ခုဆောက်မယ်။ Products CRUD + Auth (Sanctum) + proper JSON responses ပါမယ်။

1. API Endpoints Plan

MethodEndpointDescriptionAuth
POST/api/auth/registerRegister new userNo
POST/api/auth/loginLogin, get tokenNo
POST/api/auth/logoutRevoke tokenYes
GET/api/auth/meCurrent user infoYes
GET/api/productsList (search, filter, paginate)Yes
POST/api/productsCreate productYes
GET/api/products/{id}Single productYes
PUT/api/products/{id}Update productYes
DELETE/api/products/{id}Delete productYes
GET/api/categoriesList categoriesYes

2. Laravel Setup

composer create-project laravel/laravel product-api
cd product-api
composer require laravel/sanctum

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

# .env
DB_CONNECTION=mysql       # or pgsql for PostgreSQL
DB_DATABASE=product_api
DB_USERNAME=root
DB_PASSWORD=secret

# config/cors.php
'allowed_origins' => ['http://localhost:5173'],   # Vite dev server

3. Migrations

# products migration
php artisan make:migration create_products_table

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->text('description')->nullable();
    $table->decimal('price', 10, 2);
    $table->integer('stock')->default(0);
    $table->string('image')->nullable();
    $table->boolean('is_active')->default(true);
    $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
    $table->foreignId('user_id')->constrained();  // created by
    $table->timestamps();
    $table->softDeletes();  // enables deleted_at (recoverable)
});

# categories migration
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();

4. Product Model

// app/Models/Product.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name', 'description', 'price', 'stock',
        'image', 'is_active', 'category_id', 'user_id'
    ];

    protected $casts = [
        'price'     => 'decimal:2',
        'is_active' => 'boolean',
    ];

    // Relationships
    public function category() {
        return $this->belongsTo(Category::class);
    }
    public function creator() {
        return $this->belongsTo(User::class, 'user_id');
    }
}

5. API Resource (Transformer)

php artisan make:resource ProductResource

// app/Http/Resources/ProductResource.php
public function toArray(Request $request): array
{
    return [
        'id'          => $this->id,
        'name'        => $this->name,
        'description' => $this->description,
        'price'       => (float) $this->price,
        'stock'       => $this->stock,
        'is_active'   => $this->is_active,
        'image_url'   => $this->image ? asset('storage/' . $this->image) : null,
        'category'    => $this->whenLoaded('category', fn() => [
            'id'   => $this->category->id,
            'name' => $this->category->name,
        ]),
        'created_at' => $this->created_at->toISOString(),
    ];
}

// Collection Resource
php artisan make:resource ProductCollection --collection
// or just: ProductResource::collection($products)

6. ProductController (API)

php artisan make:controller Api/ProductController --resource

// app/Http/Controllers/Api/ProductController.php
public function index(Request $request)
{
    $query = Product::with('category')
        ->where('is_active', true)
        ->when($request->search, fn($q, $s) =>
            $q->where('name', 'like', "%{$s}%")
              ->orWhere('description', 'like', "%{$s}%")
        )
        ->when($request->category_id, fn($q, $id) =>
            $q->where('category_id', $id)
        )
        ->orderBy($request->sort_by ?? 'created_at', $request->sort ?? 'desc');

    return ProductResource::collection($query->paginate(15));
}

public function store(Request $request)
{
    $data = $request->validate([
        'name'        => 'required|string|max:255',
        'description' => 'nullable|string',
        'price'       => 'required|numeric|min:0',
        'stock'       => 'integer|min:0',
        'category_id' => 'nullable|exists:categories,id',
    ]);
    $data['user_id'] = auth()->id();
    $product = Product::create($data);
    return new ProductResource($product->load('category'));
}

public function update(Request $request, Product $product)
{
    $data = $request->validate([...]);  // same rules
    $product->update($data);
    return new ProductResource($product->load('category'));
}

public function destroy(Product $product)
{
    $product->delete();   // soft delete
    return response()->json(['message' => 'Deleted']);
}

7. Auth Controller (Sanctum)

// app/Http/Controllers/Api/AuthController.php
public function register(Request $request)
{
    $data = $request->validate([
        'name'     => 'required|string|max:255',
        'email'    => 'required|email|unique:users',
        'password' => 'required|string|min:8|confirmed',
    ]);
    $data['password'] = bcrypt($data['password']);
    $user  = User::create($data);
    $token = $user->createToken('api-token')->plainTextToken;
    return response()->json(['token' => $token, 'user' => $user], 201);
}

public function login(Request $request)
{
    $creds = $request->validate([
        'email'    => 'required|email',
        'password' => 'required',
    ]);
    if (!auth()->attempt($creds)) {
        return response()->json(['message' => 'Invalid credentials'], 401);
    }
    $token = auth()->user()->createToken('api-token')->plainTextToken;
    return response()->json(['token' => $token, 'user' => auth()->user()]);
}

public function logout(Request $request)
{
    $request->user()->currentAccessToken()->delete();
    return response()->json(['message' => 'Logged out']);
}

// routes/api.php
Route::prefix('auth')->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('me',      [AuthController::class, 'me']);
    });
});
Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('products',   ProductController::class);
    Route::apiResource('categories', CategoryController::class);
});

8. Testing with curl / Postman

# Register
curl -X POST http://localhost:8000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Mg Mg","email":"mg@test.com","password":"password","password_confirmation":"password"}'

# Login
curl -X POST http://localhost:8000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"mg@test.com","password":"password"}'
# response: {"token":"1|xxxx..."}

# Get products (with token)
curl http://localhost:8000/api/products \
  -H "Authorization: Bearer 1|xxxx..."

# Create product
curl -X POST http://localhost:8000/api/products \
  -H "Authorization: Bearer 1|xxxx..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Phone","price":299.99,"stock":50}'

📌 Study Checklist