Step-by-Step Guide: Build a Custom CRM from Scratch with Laravel + Vue.js (Full Code)
Quick answer
1. What is a Custom CRM and Why Build One?2. Why Laravel + Vue.js?3. Tools & Tech Stack Overview4. Project Structure & Setup4.1. 📥 Install Laravel4.2. ⚙️ Configure .env5. Backend: Laravel API Development5.1. Create Cus…

Key takeaways
- What is a Custom CRM and Why Build One?2.
- Conclusion11.1 📢 Want to Build a CRM for Your Team?
- A CRM (Customer Relationship Management) tool helps businesses manage their interactions with existing and potential customers.
1. What is a Custom CRM and Why Build One?
A CRM (Customer Relationship Management) tool helps businesses manage their interactions with existing and potential customers. Popular options like HubSpot, Salesforce, and Zoho CRM are great—but they’re often overkill, expensive, or inflexible.
Custom CRMs give businesses:
Full control of features
Seamless integration with existing tools
Cost-effective long-term scalability
Ownership of sensitive customer data
2. Why Laravel + Vue.js?
Laravel is a powerful PHP framework offering built-in routing, ORM (Eloquent), authentication, and more.
Vue.js is a lightweight frontend framework ideal for building reactive, component-driven UIs.
They’re a perfect match for Single Page Applications (SPAs) and API-driven development.
3. Tools & Tech Stack Overview
Tool | Purpose |
|---|---|
Laravel 11 | Backend API, DB, Auth |
Vue 3 + Vite | Frontend UI Components |
Axios | API Communication |
MySQL | Database |
Tailwind CSS | Styling |
Postman | API Testing |
Laravel Sanctum | API Authentication |
4. Project Structure & Setup
📥 Install Laravel
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>composer create-project laravel/laravel crm-app
cd crm-app
php artisan serve/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">composer create-project laravel/laravel crm-app
cd crm-app
php artisan serve/pre>⚙️ Configure .env
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>DB_DATABASE=crm
DB_USERNAME=root
DB_PASSWORD=/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">DB_DATABASE=crm
DB_USERNAME=root
DB_PASSWORD=/pre>Create the crm database in MySQL.5. Backend: Laravel API Development
🛠️ a) Create Customer Model & Migration
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>php artisan make:model Customer -m/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">php artisan make:model Customer -m/pre>Now edit the migration:
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function up()
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('phone')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
});
}/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">public function up()
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('phone')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
});
}/pre>Run the migration:
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>php artisan migrate/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">php artisan migrate/pre>📦 b) Customer Controller
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>php artisan make:controller API/CustomerController --api/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">php artisan make:controller API/CustomerController --api/pre>Controller Logic
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Customer;
class CustomerController extends Controller
{
public function index() {
return response()->json(Customer::all(), 200);
}
public function store(Request $request) {
$validated = $request->validate([
'name' => 'required|string',
'email' => 'required|email|unique:customers,email',
]);
$customer = Customer::create($validated + $request->only(['phone', 'notes']));
return response()->json($customer, 201);
}
public function show($id) {
return response()->json(Customer::findOrFail($id));
}
public function update(Request $request, $id) {
$customer = Customer::findOrFail($id);
$customer->update($request->all());
return response()->json($customer);
}
public function destroy($id) {
Customer::destroy($id);
return response()->json(['message' => 'Customer deleted']);
}
}/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Customer;
class CustomerController extends Controller
{
public function index() {
return response()->json(Customer::all(), 200);
}
public function store(Request $request) {
$validated = $request->validate([
'name' => 'required|string',
'email' => 'required|email|unique:customers,email',
]);
$customer = Customer::create($validated + $request->only(['phone', 'notes']));
return response()->json($customer, 201);
}
public function show($id) {
return response()->json(Customer::findOrFail($id));
}
public function update(Request $request, $id) {
$customer = Customer::findOrFail($id);
$customer->update($request->all());
return response()->json($customer);
}
public function destroy($id) {
Customer::destroy($id);
return response()->json(['message' => 'Customer deleted']);
}
}/pre>Update the Model
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>protected $fillable = ['name', 'email', 'phone', 'notes'];/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">protected $fillable = ['name', 'email', 'phone', 'notes'];/pre>🌐 c) API Routes
Update routes/api.php:
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>use App\Http\Controllers\API\CustomerController;
Route::apiResource('customers', CustomerController::class);/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">use App\Http\Controllers\API\CustomerController;
Route::apiResource('customers', CustomerController::class);/pre>🎨 6. Frontend: Vue 3 + Vite Setup
🧰 a) Install Vue 3
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>npm install
npm install vue@next vue-router@4 axios
npm run dev/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">npm install
npm install vue@next vue-router@4 axios
npm run dev/pre>resources/js/app.js
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');/pre>🔲 b) Basic Component: CustomerList.vue
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly><template>
<div>
<h2 class="text-xl font-bold mb-4">Customer List</h2>
<ul>
<li v-for="customer in customers" :key="customer.id" class="mb-2">
{{ customer.name }} – {{ customer.email }}
<button @click="deleteCustomer(customer.id)" class="text-red-600">Delete</button>
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
customers: []
}
},
mounted() {
this.getCustomers()
},
methods: {
getCustomers() {
axios.get('/api/customers').then(res => {
this.customers = res.data
})
},
deleteCustomer(id) {
axios.delete(`/api/customers/${id}`).then(() => this.getCustomers())
}
}
}
</script>/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><template>
<div>
<h2 class="text-xl font-bold mb-4">Customer List</h2>
<ul>
<li v-for="customer in customers" :key="customer.id" class="mb-2">
{{ customer.name }} – {{ customer.email }}
<button @click="deleteCustomer(customer.id)" class="text-red-600">Delete</button>
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
customers: []
}
},
mounted() {
this.getCustomers()
},
methods: {
getCustomers() {
axios.get('/api/customers').then(res => {
this.customers = res.data
})
},
deleteCustomer(id) {
axios.delete(`/api/customers/${id}`).then(() => this.getCustomers())
}
}
}
</script>/pre>🧩 App.vue
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly><template>
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-3xl font-bold mb-6">Custom CRM</h1>
<CustomerList />
</div>
</template>
<script>
import CustomerList from './components/CustomerList.vue'
export default {
components: { CustomerList }
}
</script>
/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><template>
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-3xl font-bold mb-6">Custom CRM</h1>
<CustomerList />
</div>
</template>
<script>
import CustomerList from './components/CustomerList.vue'
export default {
components: { CustomerList }
}
</script>
/pre>🔐 7. Authentication with Laravel Sanctum
Install Laravel Sanctum:
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate/pre>In api.php:
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('customers', CustomerController::class);
});/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('customers', CustomerController::class);
});/pre>🎨 8. UI Enhancements with Tailwind CSS
Install Tailwind:
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>npm install -D tailwindcss
npx tailwindcss init/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">npm install -D tailwindcss
npx tailwindcss init/pre>Add Tailwind to resources/css/app.css:
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@tailwind base;
@tailwind components;
@tailwind utilities;/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">@tailwind base;
@tailwind components;
@tailwind utilities;/pre>Update Vite config and re-run:
svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14">g fill="none" fill-rule="evenodd" transform="translate(1 1)">circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5">/circle>circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5">/circle>circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5">/circle>/g>/svg>pre class="code-block-pro-copy-button-pre" aria-hidden="true">textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>npm run dev/textarea>/pre>svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4">/path>path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">/path>/svg>pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0">npm run dev/pre>Your CRM now looks clean, responsive, and modern.
✅ 9. Testing the Custom CRM
Use Postman or Vue UI:
Create a customer → POST
/api/customersRead all customers → GET
/api/customersUpdate customer → PUT
/api/customers/{id}Delete customer → DELETE
/api/customers/{id}
🚀 10. Future Improvements
To scale this CRM further:
Add search, filters, pagination
Export customer data to CSV/PDF
Add user roles: admin, staff, sales
Build dashboards with charts (Chart.js)
🧠 11. Conclusion
By building a CRM from scratch using Laravel + Vue.js, you gain full control, flexibility, and scalability. This is ideal for teams who want software tailored to their operations—no extra features, no subscriptions, no compromises.
Whether you're a dev building your own tool or an agency like Kavcom Expert creating solutions for clients—this stack delivers.
📢 Want to Build a CRM for Your Team?
At Kavcom Expert, we build custom CRMs, dashboards, and SaaS products for businesses in the US and globally. Contact us for tailored development.
