DEV Community

Cover image for Building a real-time chat app using Laravel Reverb and Vue
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Building a real-time chat app using Laravel Reverb and Vue

Written by Rosario De Chiara✏️

Laravel Reverb is a real-time WebSocket framework that broadcasts events from Laravel to the frontend. It allows for real-time data synchronization across connected clients without page reloads. Vue is a JavaScript framework that allows for a reactive, component-based frontend experience.

In this guide, we’ll explore building a fully functional, real-time chat application using Laravel Reverb’s backend and Vue’s reactive frontend. As usual, the final, functioning code is available in this GitHub repository.

Setting up Laravel and Vue prerequisites

Before starting the development, you must set up an environment consisting of two components: the Laravel one, which is PHP-based, and the Vue/Node component.

  • PHP: Version 8.2 or above (run php -v to check the version)
  • Composer (run composer to check that it exists)
  • Node.js: Version 20 or above (run node -v to check the version)

In the following image, you can see the output of the command above on my Windows machine:

Command Output On The Windows Machine

For the database, we will use SQLite, so be sure to activate it in your php.ini file. Once you have met all the basic prerequisites, you can create a Laravel project by using the following:

> composer create-project laravel/laravel:^11.0 laravel_chat_demo
Enter fullscreen mode Exit fullscreen mode

Once you get your project root dir ready to start the development, you might need to install some more requirements; this process depends on your specific environment but, in general, try to make your composer command happy (🙂), if it complains (or just warns you) that a package is missing, install that package (Google is your friend).

Once everything is in place, you should be able to run the following:

 > php artisan serve
Enter fullscreen mode Exit fullscreen mode

This will fire up your development environment.

Building the data model for chat messages

Now that the development environment is (hopefully) fine, we can concentrate on the stimulating part of the development.

We are writing a chat web application so we can expect to handle messages. With the following command, we will get a brand new class in the /app/Models that will represent the messages exchanged in our chat:

 > php artisan make:model -m Message
Enter fullscreen mode Exit fullscreen mode

Right now, the class is empty. We will specialize it with data and functionalities, so replace the file Message.php with the following code:

<?php

namespace App\Models;

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

class Message extends Model
{
    use HasFactory;

    public $table = 'messages';
    protected $fillable = ['id', 'user_id', 'text'];

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

    public function getTimeAttribute(): string
    {
        return date(
            "d M Y, H:i:s",
            strtotime($this->attributes['created_at'])
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The code above describes a message in our system: messages will be stored in the table named messages; each message comprises three fields: id, user_id, and text. The rest of the code does two things: it creates a function that returns the user associated with the message by using the BelongsTo trait, and it defines a function that formats the field created_at in a more human-readable date and time format. It will be used on the top of each message in the chat box.

The model we just wrote will instruct Eloquent (the Laravel ORM) on how to interact with data on our database, the next step is to actually create the table on the database; to do so we need a migration, that is, a fragment of code that will create the table:

 > php artisan make:migration create_message_table
Enter fullscreen mode Exit fullscreen mode

This will create an empty migration file in database\migrations\ whose name will be something like <date and time>_create_message_table.php. The file contains two key methods: up and down. The up method introduces changes to your database schema, such as creating new tables, adding columns, or establishing indexes.

Conversely, the down method's role is to undo or reverse the modifications made by the corresponding up method, effectively allowing you to roll back changes if needed. We specialize this file with the following code:

 <?php

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

return new class extends Migration {
    public function up(): void {
        Schema::create('messages', function (Blueprint $table) {
            $table->id();
            $table->timestamps();

            $table->foreignId('user_id')->constrained();
            $table->text('text')->nullable();
        });
    }

    public function down(): void {
        Schema::dropIfExists('messages');
    }
};
Enter fullscreen mode Exit fullscreen mode

This migration defines a new messages table with some fields: an auto-incrementing ID as the primary key, a user_id that references the users table to link messages with their senders, and a text field to store the message content.

The table also incorporates timestamp columns to track when each message is created and modified automatically; timestamps will create the two fields created_at and updated_at. As a precautionary measure, the operation includes a reversal method that can remove the messages table if needed, allowing for easy rollback of these changes.

At this point, let’s run the following:

 > php artisan migrate:fresh
Enter fullscreen mode Exit fullscreen mode

This will create a new table in the database. If you want to check that it was successful, simply open the database\database.sqlite file.

Adding user registration and login

Now that we have room for data, it is time to create the frontend. When developing the user interface for a Laravel application, you have two main options: the first involves using PHP to construct your frontend. In contrast, the second uses JavaScript frameworks like Vue or React.

We will use Vue in this article. This will also allow developers to take advantage of the huge packages ecosystem and tools available via npm. First, we need to install the appropriate package:

 > composer require laravel/ui
Enter fullscreen mode Exit fullscreen mode

Once the laravel/ui package has been installed, you may install the frontend scaffolding using the artisan command: the following command generates the UI for handling the registration and the login to our web app; just consider how complex this task can be if you should write this from scratch and how incredibly easy it is handled in Laravel:

 > php artisan ui vue --auth
Enter fullscreen mode Exit fullscreen mode

At this point, we have the two halves of the project in place: the PHP is already running with the PHP artisan serve command; now it is the moment to run the JavaScript-based part with:

 > npm install
Enter fullscreen mode Exit fullscreen mode

To have both the parts running, open two shell windows; one running the artisan command, and the other with the following command:

 > npm run dev
Enter fullscreen mode Exit fullscreen mode

This command will keep the frontend running and reload it every time we modify it.

At the end of this process, without writing a single line of code, we have a fully functional website with the possibility of handling user authentication. Go to http://127.0.0.1:8000/register to register a new user and log in to the website:

User Login Page

Configuring routes for the chat message API

Now, we need to add routes for the different APIs we are going to host:

  • /home for the home page – this should already be present
  • /message, a POST HTTP method for adding a new message
  • /messages to GET all the existing messages

We will modify the /routes/web.php as follows:

<?php

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;

Route::get('/', function () { return view('welcome'); });

Auth::routes();

Route::get('/home', [HomeController::class, 'index'])
    ->name('home');
Route::get('/messages', [HomeController::class, 'messages'])
    ->name('messages');
Route::post('/message', [HomeController::class, 'message'])
    ->name('message');
Enter fullscreen mode Exit fullscreen mode

Now, it is time to write the HomeController that will implement the APIs we described above:

<?php

namespace App\Http\Controllers;

use App\Jobs\SendMessage;
use App\Models\Message;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class HomeController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function index()
    {
        $user = User::where('id', auth()->id())->select([
            'id',
            'name',
            'email',
        ])->first();

        return view('home', [
            'user' => $user,
        ]);
    }

    public function messages(): JsonResponse
    {
        $messages = Message::with('user')->get()->append('time');

        return response()->json($messages);
    }

    public function message(Request $request): JsonResponse
    {
        $message = Message::create([
            'user_id' => auth()->id(),
            'text' => $request->get('text'),
        ]);
        SendMessage::dispatch($message);

        return response()->json([
            'success' => true,
            'message' => "Message created and job dispatched.",
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here you can see the logic behind the APIs described above:

  • In the /home method, we retrieve the logged-in user's information from the database using the User model and pass it to the view
  • In the /messages method, we fetch all messages from the database via the Message model, include the related user data, add the time field (using an accessor) to each message, and send the complete set to the view
  • In the /message method, we'll create a new message in the database using the Message model and dispatch the SendMessage queue job

When everything is set, we can install and configure Laravel events and queue jobs to host the exchange and synchronization of messages.

Configuring Laravel event and queue job

Laravel's event and queue job systems provide powerful tools for handling asynchronous tasks and decoupling various parts of an application.

Events allow you to define and broadcast specific actions or changes within your application, which listeners can then respond to. Queue jobs enable you to offload time-consuming tasks, such as sending emails or processing large datasets, to background workers, ensuring that your application remains responsive and efficient.

Together, these features enhance scalability and improve the overall user experience by managing processes in the background. We will use both: the event is essentially a data container that holds the message, and the QueueListener handles the number of messages waiting to be dispatched.

With the following command, we generate the Event class in the /app/Events directory:

 > php artisan make:event GotMessage
Enter fullscreen mode Exit fullscreen mode

Then we have to implement two things: the constructor that will describe what is the payload of this event, and the broadcastOn() method to specify on which channel these events will be broadcasted:

<?php

namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class GotMessage implements ShouldBroadcast {
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct() {
    }

    public function broadcastOn() {
        return  new PrivateChannel("channel_for_everyone");
    }
}
Enter fullscreen mode Exit fullscreen mode

The name of the channel ("channel_for_everyone") will be used in the file describing the WebSocket (see below) that will represent the communication channel between each instance of the chat client and the server. Note here that the constructor takes no parameters and there is no reference to the messages: the idea is that this event will be broadcasted once a new message has been sent, once the client receives it, they will just request the updated list of messages to the server using the services we implemented before.

To generate the QueueListener, we use the following:

 > php artisan make:job SendMessage
Enter fullscreen mode Exit fullscreen mode

This command generates the SendMessage.php file in the /app/Jobs directory. This file instructs the Laravel framework on how to handle newly created instances of the GotMessage event that we defined earlier:

<?php

namespace App\Jobs;

use App\Events\GotMessage;
use App\Models\Message;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendMessage implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public Message $message) {
        //
    }

    public function handle(): void {
        GotMessage::dispatch([
            'id' => $this->message->id,
            'user_id' => $this->message->user_id,
            'text' => $this->message->text,
            'time' => $this->message->time,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The handle() method dispatches the GotMessage event with details such as the message id, user_id, text, and timestamp. This job is designed to run in the background asynchronously processing events as long as they are enqueued on the channel, enabling efficient handling of message-sending tasks in the background. As you can see, there is a public $message property as a constructor argument; this helps implement the queue process more efficiently (see the documentation for additional details).

With all other components in place, we now just need to add the final PHP element for the project: the WebSocket.

Adding Laravel Reverb

Laravel Reverb brings a real-time WebSocket framework that allows you to send events from your Laravel application to the frontend using WebSockets. With Reverb, it is possible to broadcast the events we defined above, reflected on each client connected, without requiring a page reload. As usual, adding this quite complex feature is addressed with one simple command:

 > php artisan install:broadcasting
Enter fullscreen mode Exit fullscreen mode

By accepting the default option, it will install both the PHP part for the backend and the Node dependencies to be used on the frontend.

Running this command will make several changes to your project directory: it will add a new section to the .env file for Reverb, create a reverb.php file in /config to read these new fields, and, most importantly, add a channels.php file in /routes. In this channels.php file, we’ll configure Reverb to create the channel_for_everyone channel by adding the following code:

Broadcast::channel('channel_for_everyone', function ($user) {
    return true;
});
Enter fullscreen mode Exit fullscreen mode

At this point, everything is in place on the backend. Now we can focus on the frontend.

Building the Vue frontend

In this section, we will design a simple frontend, focusing on functionality and integration, without any styling, font customization, etc.

The first step is to set up the Vue environment:

> npm install vue vue-router @vitejs/plugin-vue```
{% endraw %}


Now we can focus on three files to integrate a Vue template in Laravel. The first one is {% raw %}`resources/js/app.js`{% endraw %}, which will instantiate the Vue component that contains our app:
{% raw %}


```javascript 
import './bootstrap';
import App from './App.vue'
import { createApp } from 'vue';

const app = createApp({});

app.component('app', App);

app.mount("#app");
Enter fullscreen mode Exit fullscreen mode

The code is simple: App.vue is our Vue template and we just associate it with the div whose id is #app in the blade file (see the next code snippet) that will render our web app. The next file is resources/js/App.vue, which contains the template:

<script setup>
const props = defineProps({
    isAuth: {
        type: Boolean,
        default: false
    },
    user: {
        type: [Object, Array],
        required: false
    }
})
</script>
<template>
    <h1>Hello, {{ user.name }}</h1>
</template>
Enter fullscreen mode Exit fullscreen mode

The code does two things: it defines the props for Vue to host the values coming from Laravel, and then displays a simple message. As you can see, we just received the current authenticated user and show their name.

The last file is the blade that will glue together the Vue component and the data we want to feed to it. Let’s just copy the following code in the resources\views\welcome.blade.php file:

<!DOCTYPE html>

<head> 
 <title>Laravel + Vue Chat</title>
 @vite(['resources/js/app.js'])
</head>

<body>
 <div class="min-h-screen bg-gray-100" id="app">
  <header >
   @if (Route::has('login'))
    <nav>
     @auth
      <div id="app">
       <app :is-auth="{{ json_encode(auth()->check()) }}"
            :user="{{ auth()->check() ? auth()->user() : 'null' }}">>
       </app>
    { ... Logout code .. }
      </div>
     @else
      <a href="{{ route('login') }}"> Log in </a>
      @if (Route::has('register'))
       <a href="{{ route('register') }}"> Register </a>
      @endif
     @endauth
     </nav>
    @endif
   </header>
  </div>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

In the code above, we first include the app.js file to access the Vue component we just created. Then it renders the Login/Register options if the user is not logged in or passes the is-auth and user variables to the props we defined in the resources/js/App.vue file above.

In the chunk of code above, the Logout code is missing. Check the repository for additional details.

To verify that everything works, you will need to keep running four server components in four different shell windows:

  • Build frontend assets for the Vue components:
 > npm run build```
{% endraw %}


*   Start listening to the Laravel events:
{% raw %}


```shell 
 > php artisan queue:listen```
{% endraw %}


*   Start the Reverb WebSocket server:
{% raw %}


```shell 
 > php artisan reverb:start```
{% endraw %}


*   Start the Laravel server:
{% raw %}


```shell 
 > php artisan serve
Enter fullscreen mode Exit fullscreen mode

Open your browser, and go to http://127.0.0.1:8000/; our brand-new chat app will appear. In the image below, you can see the four services running one beside the other:

The Four Services Running One Beside The Other

If everything is in place, you can show in your browser the following message (keep in mind that the name will be different! Unless, of course, you sign in with the name Rosario 🙂):

Greeting Message In The App Login Page

Before finalizing the application, I’ll give you two useful tips if you incur problems during the development:

  • Have a look at the file storage\logs\laravel.log; this is where you can write logs with the Log facility and where each component of your system will complain if something is not working
  • Launch the reverb:start with php artisan reverb:start --debug to have more debug messages

Now it is time to complete the frontend with all the required pieces of a chat web app: a text box of new messages and a list of messages:

<script>
import axios from 'axios';

export default {
    data() {
        return {
            messages: [],
            newMessage: "",
        };
    },
    methods: {
        async postMessage(text) {
            try {
                await axios.post(`/message`, {
                    text,
                });
                // After posting, retrieve messages to include the new one
                this.getMessages();
            } catch (err) {
                console.log(err.message);
            }
        },
        async getMessages() {
            try {
                const response = await axios.get('/messages');
                this.messages = response.data;
                // Scroll to the bottom after messages are updated
                this.scrollToBottom();
            } catch (err) {
                console.log(err.message);
            }
        },
        sendMessage() {
            if (this.newMessage.trim() !== "") {
                this.postMessage(this.newMessage.trim());
                this.newMessage = "";
            } else {
                return;
            }
        },
        scrollToBottom() {
            this.$nextTick(() => {
                const messageList = document.getElementById('messagelist');
                if (messageList) {
                    messageList.scrollTop = messageList.scrollHeight;
                }
            });
        }
    },
    created() {
        this.getMessages();

        window.Echo.private("channel_for_everyone")
            .listen('GotMessage', (e) => {
                this.getMessages();
            });
    },
};
</script>

<template>
    <div class="container">
        <div class="chat-box" id="messagelist">
            <div v-for="(message, index) in messages" :key="index" class="message">
                <strong>{{ message.user.name }}:</strong> {{ message.text }}
                <small class="text-muted float-right">{{ message.time }}</small>
            </div>
        </div>
        <div class="input-area">
            <input v-model="newMessage" @keyup.enter="sendMessage" type="text"
                placeholder="Type your message here..." />
            <button @click="sendMessage">Send</button>
        </div>
    </div>
</template>

<style scoped>
.chat-box {
    border: 1px solid #ccc;
    padding: 10px;
    max-height: 300px;
    overflow-y: auto;
}

.message {
    margin-bottom: 10px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

The code, as you can see, is a bit longer but it is fairly simple:

  • The data method defines the two global objects we manipulate here, the messages array that contains the messages already sent to the chat and the newMessage that contains the new text message sent by the user
  • The four methods are:
    • postMessage to do an HTTP POST to the /message API we defined above to send a message text to the backend
    • getMessages, which invokes the /getMessages API to get the messages in the DB
    • sendMessage, which invokes the async method postMessage and cleans the textbox once the message has been POSTed to the backend
    • scrollToBottom, which just scrolls the list of messages to the last message received by the backend
  • created is the method that runs when the component is first created. It updates the list of existing messages by invoking getMessage and then instructs the Echo service (which listens on the WebSocket) to subscribe to new events on the channel_for_everyone channel we defined above

After the methods’ definition, you can see the template of the Vue component with very simple HTML for the message box the text box for new messages, and a little CSS to style everything.

The following image shows the final UI of the chat web app running in the browser:

Final UI Of The Chat Web App

Conclusion

In this project, we built a real-time chat app using Laravel Reverb and Vue, demonstrating how to integrate Laravel’s event-driven backend with Vue’s reactive frontend. Use the information learned here to develop scalable, interactive web applications. Explore the final code on GitHub to see Laravel Reverb and Vue in action.


Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.

LogRocket Vue Demonstration

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps - Start monitoring for free.

Top comments (0)