630 lines
16 KiB
Markdown
630 lines
16 KiB
Markdown
# 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 │
|
|
└────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## 1. Hybrid Approach (Recommended)
|
|
|
|
### Konsep
|
|
Kombinasi antara SPA (Vue.js) untuk user interface dan Laravel API untuk data handling.
|
|
|
|
### Routes Structure
|
|
```php
|
|
// 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
|
|
<?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
|
|
```javascript
|
|
// 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
|
|
```javascript
|
|
// 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
|
|
```vue
|
|
<!-- 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
|
|
```bash
|
|
# Install Sanctum
|
|
composer require laravel/sanctum
|
|
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
|
|
php artisan migrate
|
|
```
|
|
|
|
### Auth Controller
|
|
```php
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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! |