16 KiB
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 │
└────────────────────────────────────────────────────────────┘
1. Hybrid Approach (Recommended)
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
- API Versioning - Gunakan
/api/v1/
prefix - Resource Controllers - Gunakan
apiResource()
untuk CRUD - Service Layer - Pisahkan business logic dari controller
- Validation - Gunakan Form Requests untuk validasi complex
- Caching - Implement caching untuk data yang sering diakses
- Queue Jobs - Untuk operasi yang memakan waktu
- Event/Listeners - Untuk decoupling business logic
Konsep ini memberikan fleksibilitas tinggi dengan maintenance yang relatif mudah!