Kasir/DOC-BACKEND.md
2025-08-27 16:32:02 +07:00

16 KiB

Laravel + Vue.js Monolith Backend Concepts

Arsitektur Overview

┌────────────────────────────────────────────────────────────┐
│                    LARAVEL MONOLITH                        │
├────────────────────────────────────────────────────────────┤
│  Frontend (SPA)           │  Backend (API + Web)           │
│  ─────────────────        │  ──────────────────            │
│  • Vue.js Components      │  • Controllers                 │
│  • Vue Router             │  • Models                      │
│  • Axios/HTTP Client      │  • Migrations                  │
│  • State Management       │  • Services                    │
│                           │  • Jobs/Queues                 │
│                           │  • Middleware                  │
└────────────────────────────────────────────────────────────┘

Konsep

Kombinasi antara SPA (Vue.js) untuk user interface dan Laravel API untuk data handling.

Routes Structure

// routes/web.php
<?php

use Illuminate\Support\Facades\Route;

// API Routes untuk Vue.js
Route::prefix('api')->middleware('api')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::apiResource('posts', PostController::class);
    Route::apiResource('users', UserController::class);
});

// Blade Routes (jika diperlukan)
Route::get('/admin', function () {
    return view('admin.dashboard'); // Traditional Blade view
});

// SPA Catch-all (harus paling bawah)
Route::get('/{any}', function () {
    return view('spa'); // Vue.js SPA
})->where('any', '^(?!api|admin).*$'); // Exclude api & admin routes

Controller Example

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class PostController extends Controller
{
    public function index(): JsonResponse
    {
        $posts = Post::with('author')
            ->latest()
            ->paginate(10);
            
        return response()->json($posts);
    }
    
    public function store(Request $request): JsonResponse
    {
        $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);
        
        $post = Post::create([
            'title' => $request->title,
            'content' => $request->content,
            'user_id' => auth()->id(),
        ]);
        
        return response()->json($post->load('author'), 201);
    }
    
    public function show(Post $post): JsonResponse
    {
        return response()->json($post->load('author'));
    }
    
    public function update(Request $request, Post $post): JsonResponse
    {
        $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);
        
        $post->update($request->only(['title', 'content']));
        
        return response()->json($post->load('author'));
    }
    
    public function destroy(Post $post): JsonResponse
    {
        $post->delete();
        
        return response()->json(['message' => 'Post deleted successfully']);
    }
}

2. Frontend Integration

Axios Setup

// resources/js/services/api.js
import axios from 'axios'

const api = axios.create({
    baseURL: '/api',
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    }
})

// Add CSRF token for Laravel
const token = document.head.querySelector('meta[name="csrf-token"]')
if (token) {
    api.defaults.headers.common['X-CSRF-TOKEN'] = token.content
}

// Request interceptor
api.interceptors.request.use((config) => {
    const auth = localStorage.getItem('auth_token')
    if (auth) {
        config.headers.Authorization = `Bearer ${auth}`
    }
    return config
})

// Response interceptor
api.interceptors.response.use(
    (response) => response,
    (error) => {
        if (error.response?.status === 401) {
            // Handle unauthorized
            localStorage.removeItem('auth_token')
            window.location.href = '/login'
        }
        return Promise.reject(error)
    }
)

export default api

Vue Service Example

// resources/js/services/postService.js
import api from './api'

export const postService = {
    // Get all posts
    async getPosts(page = 1) {
        const response = await api.get(`/posts?page=${page}`)
        return response.data
    },
    
    // Get single post
    async getPost(id) {
        const response = await api.get(`/posts/${id}`)
        return response.data
    },
    
    // Create post
    async createPost(postData) {
        const response = await api.post('/posts', postData)
        return response.data
    },
    
    // Update post
    async updatePost(id, postData) {
        const response = await api.put(`/posts/${id}`, postData)
        return response.data
    },
    
    // Delete post
    async deletePost(id) {
        const response = await api.delete(`/posts/${id}`)
        return response.data
    }
}

Vue Component with API Integration

<!-- resources/js/pages/Posts.vue -->
<template>
  <div class="posts">
    <div class="flex justify-between items-center mb-6">
      <h1 class="text-3xl font-bold">Posts</h1>
      <button 
        @click="showCreateForm = true"
        class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
      >
        Create Post
      </button>
    </div>
    
    <!-- Loading State -->
    <div v-if="loading" class="text-center py-8">
      Loading posts...
    </div>
    
    <!-- Posts List -->
    <div v-else class="grid gap-4">
      <div 
        v-for="post in posts" 
        :key="post.id"
        class="bg-white p-6 rounded-lg shadow"
      >
        <h3 class="text-xl font-semibold mb-2">{{ post.title }}</h3>
        <p class="text-gray-600 mb-4">{{ post.content }}</p>
        <div class="flex justify-between items-center">
          <span class="text-sm text-gray-500">
            By {{ post.author.name }}
          </span>
          <div class="space-x-2">
            <button 
              @click="editPost(post)"
              class="text-blue-500 hover:text-blue-700"
            >
              Edit
            </button>
            <button 
              @click="deletePost(post.id)"
              class="text-red-500 hover:text-red-700"
            >
              Delete
            </button>
          </div>
        </div>
      </div>
    </div>
    
    <!-- Pagination -->
    <div v-if="pagination.last_page > 1" class="mt-6 flex justify-center">
      <nav class="flex space-x-2">
        <button 
          v-for="page in pagination.last_page"
          :key="page"
          @click="loadPosts(page)"
          :class="[
            'px-3 py-2 rounded',
            page === pagination.current_page 
              ? 'bg-blue-500 text-white' 
              : 'bg-gray-200 hover:bg-gray-300'
          ]"
        >
          {{ page }}
        </button>
      </nav>
    </div>
    
    <!-- Create/Edit Modal -->
    <div v-if="showCreateForm" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
      <div class="bg-white p-6 rounded-lg w-full max-w-md">
        <h2 class="text-xl font-bold mb-4">
          {{ editingPost ? 'Edit Post' : 'Create Post' }}
        </h2>
        <form @submit.prevent="submitForm">
          <div class="mb-4">
            <label class="block text-sm font-medium mb-2">Title</label>
            <input 
              v-model="form.title"
              type="text" 
              class="w-full border rounded px-3 py-2"
              required
            >
          </div>
          <div class="mb-4">
            <label class="block text-sm font-medium mb-2">Content</label>
            <textarea 
              v-model="form.content"
              class="w-full border rounded px-3 py-2 h-32"
              required
            ></textarea>
          </div>
          <div class="flex justify-end space-x-2">
            <button 
              type="button"
              @click="cancelForm"
              class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
            >
              Cancel
            </button>
            <button 
              type="submit"
              class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
            >
              {{ editingPost ? 'Update' : 'Create' }}
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { postService } from '../services/postService'

const posts = ref([])
const loading = ref(false)
const showCreateForm = ref(false)
const editingPost = ref(null)
const pagination = ref({})

const form = reactive({
  title: '',
  content: ''
})

const loadPosts = async (page = 1) => {
  loading.value = true
  try {
    const data = await postService.getPosts(page)
    posts.value = data.data
    pagination.value = {
      current_page: data.current_page,
      last_page: data.last_page,
      total: data.total
    }
  } catch (error) {
    console.error('Error loading posts:', error)
  } finally {
    loading.value = false
  }
}

const submitForm = async () => {
  try {
    if (editingPost.value) {
      await postService.updatePost(editingPost.value.id, form)
    } else {
      await postService.createPost(form)
    }
    
    cancelForm()
    loadPosts()
  } catch (error) {
    console.error('Error submitting form:', error)
  }
}

const editPost = (post) => {
  editingPost.value = post
  form.title = post.title
  form.content = post.content
  showCreateForm.value = true
}

const deletePost = async (id) => {
  if (confirm('Are you sure?')) {
    try {
      await postService.deletePost(id)
      loadPosts()
    } catch (error) {
      console.error('Error deleting post:', error)
    }
  }
}

const cancelForm = () => {
  showCreateForm.value = false
  editingPost.value = null
  form.title = ''
  form.content = ''
}

onMounted(() => {
  loadPosts()
})
</script>

3. Authentication Integration

Laravel Sanctum Setup

# Install Sanctum
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Auth Controller

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
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',
        ]);

        $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('auth_token')->plainTextToken;

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

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

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

    public function me(Request $request)
    {
        return response()->json($request->user());
    }
}

4. Database Structure

Migration Example

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->string('slug')->unique();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->boolean('published')->default(false);
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
            
            $table->index(['published', 'published_at']);
        });
    }

    public function down()
    {
        Schema::dropIfExists('posts');
    }
};

Model with Relationships

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'content',
        'slug',
        'user_id',
        'published',
        'published_at',
    ];

    protected $casts = [
        'published' => 'boolean',
        'published_at' => 'datetime',
    ];

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function getRouteKeyName()
    {
        return 'slug';
    }
}

5. Service Layer Pattern

Service Class

<?php

namespace App\Services;

use App\Models\Post;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Str;

class PostService
{
    public function getAllPosts(int $perPage = 10): LengthAwarePaginator
    {
        return Post::with('author')
            ->where('published', true)
            ->latest('published_at')
            ->paginate($perPage);
    }

    public function createPost(array $data): Post
    {
        $data['slug'] = Str::slug($data['title']);
        $data['user_id'] = auth()->id();
        
        return Post::create($data);
    }

    public function updatePost(Post $post, array $data): Post
    {
        if (isset($data['title'])) {
            $data['slug'] = Str::slug($data['title']);
        }
        
        $post->update($data);
        
        return $post->fresh();
    }

    public function deletePost(Post $post): bool
    {
        return $post->delete();
    }
}

6. Middleware untuk API

Custom API Middleware

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class ApiResponse
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);
        
        // Add consistent API response format
        if ($request->expectsJson()) {
            $data = $response->getData();
            
            return response()->json([
                'success' => $response->status() < 400,
                'data' => $data,
                'message' => $response->status() < 400 ? 'Success' : 'Error',
                'status_code' => $response->status(),
            ], $response->status());
        }
        
        return $response;
    }
}

Key Benefits Monolith

Single Deployment - Satu aplikasi, satu deploy Shared Authentication - Session/token bersama Database Consistency - Satu database, konsisten Easier Development - Setup dan maintenance lebih mudah Performance - Tidak ada network latency antar service CSRF Protection - Built-in Laravel CSRF File Sharing - Storage dan assets bersama

Best Practices

  1. API Versioning - Gunakan /api/v1/ prefix
  2. Resource Controllers - Gunakan apiResource() untuk CRUD
  3. Service Layer - Pisahkan business logic dari controller
  4. Validation - Gunakan Form Requests untuk validasi complex
  5. Caching - Implement caching untuk data yang sering diakses
  6. Queue Jobs - Untuk operasi yang memakan waktu
  7. Event/Listeners - Untuk decoupling business logic

Konsep ini memberikan fleksibilitas tinggi dengan maintenance yang relatif mudah!