Shoutbox sederhana dengan:
- Backend: Laravel 12 (REST API)
- Frontend: React + Vite + Tailwind
- Database: PostgreSQL
- Refresh: polling 5 detik
- PHP ≥ 8.2, Composer
- Node.js ≥ 18, pnpm atau npm
- PostgreSQL
- Git
git clone <repo-anda> shoutbox
cd shoutbox
composer install
cp .env.example .env
php artisan key:generateEdit .env (contoh untuk PostgreSQL):
APP_NAME=Shoutbox
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=shoutbox
DB_USERNAME=postgres
DB_PASSWORD=postgresBuat database:
CREATE DATABASE shoutbox;Jalankan migrasi:
php artisan migrateJalankan server:
php artisan serve
# Akses: http://localhost:8000-
Migration:
database/migrations/2025_10_10_170921_create_messages_table.php- Kolom:
id,username,content,timestamps
- Kolom:
-
Model:
app/Models/Message.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
protected $fillable = ['username', 'content'];
}- Controller:
app/Http/Controllers/MessageController.phpindex()-> return semua pesan (terlama dulu)store()-> validasi, simpan, return 201
<?php
namespace App\Http\Controllers;
use App\Models\Message;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class MessageController extends Controller
{
public function index()
{
return [
'success' => true,
'data' => Message::oldest()->get(),
];
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'username' => 'required|string|max:20',
'content' => 'required|string|max:120',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$message = Message::create([
'username' => $request->username,
'content' => $request->content,
]);
return response()->json([
'success' => true,
'data' => $message,
], 201);
}
}- Routes API:
routes/api.php
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\MessageController;
Route::get('/messages', [MessageController::class, 'index']);
Route::post('/messages', [MessageController::class, 'store']);Uji cepat API:
curl http://localhost:8000/api/messages
curl -X POST http://localhost:8000/api/messages \
-H "Content-Type: application/json" \
-d '{"username":"alice","content":"Halo!"}'Masuk ke folder frontend:
cd frontend
pnpm install
# atau: npm installSet konfigurasi endpoint: frontend/src/config/index.js
// Saat lokal:
export const BASE_URL = "http://localhost:8000";
export const API_URL = `${BASE_URL}/api`;Catatan: Di repo saat ini di-set ke "https://shoutbox.test". Ganti ke http://localhost:8000 untuk dev lokal.
Jalankan frontend:
pnpm dev
# atau: npm run dev
# Akses: http://localhost:5173- File:
frontend/src/components/shoutbox.jsx - Fitur:
- Ambil pesan (
GET /api/messages) - Kirim pesan (
POST /api/messages) - Simpan
usernamedilocalStorage - Auto-refresh tiap 5 detik, auto-scroll ke pesan terbaru
- Ambil pesan (
Potongan inti:
import { useEffect, useRef, useState } from "react";
import axios from "axios";
import { API_URL } from "../config";
export default function Shoutbox() {
const messageEndsRef = useRef(null);
const [messages, setMessages] = useState([]);
const [username, setUsername] = useState(localStorage.getItem("username") || "");
const [content, setContent] = useState("");
const [errors, setErrors] = useState([]);
useEffect(() => { fetchMessages(); }, []);
useEffect(() => { localStorage.setItem("username", username); }, [username]);
useEffect(() => {
const interval = setInterval(fetchMessages, 5000);
return () => clearInterval(interval);
}, []);
const fetchMessages = async () => {
const { data: { data } } = await axios.get(`${API_URL}/messages`);
setMessages(data);
};
const sendMessage = async (e) => {
e.preventDefault();
try {
const { data: { data } } = await axios.post(`${API_URL}/messages`, { username, content });
setContent("");
setMessages([...messages, data]);
setTimeout(() => messageEndsRef.current?.scrollIntoView({ behavior: "smooth" }), 100);
} catch (error) {
setErrors(error.response?.data?.errors || []);
}
};
return (
<div className="max-w-lg mx-auto mt-10 p-4 border rounded">
<h1 className="text-xl font-bold mb-4 text-center">💬 Shoutbox</h1>
<div className="h-64 overflow-y-auto border p-2 mb-4 bg-slate-50 rounded">
{messages.map((msg) => (
<div key={msg.id} className="mb-2 flex flex-col gap-1">
<strong>{msg.username}</strong>
<div>{msg.content}</div>
<span className="text-xs text-slate-400">
{new Date(msg.created_at).toLocaleTimeString("en-US", { hour12: false }).slice(0, 5)}
</span>
</div>
))}
<div ref={messageEndsRef} />
</div>
<form onSubmit={sendMessage} className="flex flex-col gap-3">
<input
type="text"
placeholder="Enter username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="border p-2 flex-1 rounded"
/>
{errors.username && <div className="text-red-500">{errors.username}</div>}
<textarea
value={content}
rows={3}
placeholder="Wow, this is amazing..."
onChange={(e) => setContent(e.target.value)}
className="border p-2 flex-1 rounded"
/>
{errors.content && <div className="text-red-500">{errors.content}</div>}
<button className="bg-blue-600 text-white px-4 py-2 rounded">Send</button>
</form>
</div>
);
}- Entry app:
frontend/src/App.jsx-> renderShoutboxfrontend/src/main.jsx-> mount ke#rootfrontend/index.html-> container root
- Terminal 1 (Backend):
php artisan serve
# http://localhost:8000- Terminal 2 (Frontend):
cd frontend
pnpm dev
# http://localhost:5173Buka http://localhost:5173, isi username, kirim pesan, cek daftar pesan tampil dan refresh tiap 5 detik.
Jika frontend (5173) berbeda origin dengan backend (8000) dan terjadi CORS error:
- Pastikan backend mengizinkan origin dev Vite Anda.
- Terapkan middleware CORS default Laravel (
HandleCors) beserta konfigurasi yang sesuai. Jika fileconfig/cors.phpbelum ada, tambahkan paket/config CORS sesuai dokumentasi Laravel. - Alternatif: samakan origin (mis. serve frontend lewat proxy yang sama).
shoutbox/
├── app/
│ ├── Http/Controllers/MessageController.php
│ └── Models/Message.php
├── database/migrations/2025_10_10_170921_create_messages_table.php
├── routes/api.php
├── frontend/
│ ├── src/
│ │ ├── components/shoutbox.jsx
│ │ ├── config/index.js
│ │ ├── App.jsx
│ │ └── main.jsx
│ └── index.html
└── .env
-
Database:
- Cek
.env(DB host, port, user, pass, nama DB) - Pastikan service PostgreSQL aktif
- Jalankan
php artisan migrateulang bila perlu
- Cek
-
CORS:
- Pastikan origin
http://localhost:5173diizinkan
- Pastikan origin
-
Frontend tidak bisa fetch:
- Periksa
frontend/src/config/index.js(BASE_URL) - Cek console browser dan Network tab
- Pastikan backend berjalan di
http://localhost:8000
- Periksa
Implementasi real-time menggunakan Laravel Reverb, solusi WebSocket native Laravel yang blazing fast dan mudah dikonfigurasi.
-
Install Laravel Reverb:
composer require laravel/reverb:@beta php artisan reverb:install
-
Konfigurasi
.env:BROADCAST_CONNECTION=reverb REVERB_APP_ID=my-app-id REVERB_APP_KEY=my-app-key REVERB_APP_SECRET=my-app-secret REVERB_HOST=localhost REVERB_PORT=8080 REVERB_SCHEME=http VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_HOST="${REVERB_HOST}" VITE_REVERB_PORT="${REVERB_PORT}" VITE_REVERB_SCHEME="${REVERB_SCHEME}"
-
Buat Event Broadcast:
php artisan make:event MessageCreated
Edit
app/Events/MessageCreated.php:<?php namespace App\Events; use App\Models\Message; use Illuminate\Broadcasting\Channel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class MessageCreated implements ShouldBroadcast { use Dispatchable, SerializesModels; public function __construct(public Message $message) {} public function broadcastOn(): Channel { return new Channel('shoutbox'); } public function broadcastAs(): string { return 'MessageCreated'; } public function broadcastWith(): array { return [ 'id' => $this->message->id, 'username' => $this->message->username, 'content' => $this->message->content, 'created_at' => $this->message->created_at, ]; } }
-
Trigger Event di Controller:
Edit
app/Http/Controllers/MessageController.php, tambahkan di methodstore():use App\Events\MessageCreated; // Setelah $message dibuat event(new MessageCreated($message)); return response()->json([ 'success' => true, 'data' => $message, ], 201);
-
Jalankan Reverb Server:
php artisan reverb:start
Server WebSocket akan berjalan di
http://localhost:8080
-
Install Dependencies:
cd frontend pnpm add laravel-echo pusher-js -
Buat Config Echo (
frontend/src/config/echo.js):import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; export const echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: import.meta.env.VITE_REVERB_HOST, wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', enabledTransports: ['ws', 'wss'], });
-
Tambah
.envdi Frontend:Buat
frontend/.env:VITE_REVERB_APP_KEY=my-app-key VITE_REVERB_HOST=localhost VITE_REVERB_PORT=8080 VITE_REVERB_SCHEME=http
-
Subscribe Channel di Komponen:
Edit
frontend/src/components/shoutbox.jsx, tambahkan:import { echo } from '../config/echo'; // Tambahkan useEffect untuk subscribe useEffect(() => { const channel = echo.channel('shoutbox') .listen('MessageCreated', (e) => { console.log('New message received:', e); setMessages((prev) => [...prev, e]); // Auto scroll setTimeout(() => { messageEndsRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 100); }); return () => { echo.leave('shoutbox'); }; }, []);
-
Nonaktifkan Polling (Opsional):
Hapus atau comment interval polling:
// useEffect(() => { // const interval = setInterval(fetchMessages, 5000); // return () => clearInterval(interval); // }, []);
Dan hapus duplikasi setelah POST:
const sendMessage = async (e) => { e.preventDefault(); try { await axios.post(`${API_URL}/messages`, { username, content }); setContent(""); // Hapus: setMessages([...messages, data]); // Biarkan WebSocket yang update } catch (error) { setErrors(error.response?.data?.errors || []); } };
-
Terminal 1 - Backend:
php artisan serve
-
Terminal 2 - Reverb Server:
php artisan reverb:start
-
Terminal 3 - Frontend:
cd frontend pnpm dev -
Test Real-time:
- Buka 2 tab browser di
http://localhost:5173 - Kirim pesan dari tab pertama
- Pesan akan muncul instant di tab kedua tanpa refresh!
- Buka 2 tab browser di
- Connection refused: Pastikan Reverb server berjalan di port 8080
- CORS error: Reverb sudah handle CORS secara default
- Event tidak terdengar: Cek nama channel dan event name harus sama persis
- Reverb crash: Restart dengan
php artisan reverb:start --debug
- Real-time via WebSocket (Pusher/Laravel Echo)
- [] Rate limiting untuk anti-spam
- [] Hapus/Edit pesan milik sendiri
- [] Authentication (Laravel Sanctum)
- [] Pagination & infinite scroll
- [] Emoji picker, attachment
-
.envsudah diisi danphp artisan key:generate -
php artisan migratesukses - Backend live di
http://localhost:8000 -
frontend/src/config/index.jspakaihttp://localhost:8000 - Frontend live di
http://localhost:5173 - Kirim pesan berhasil dan tampil