Building a complete CRUD (Create, Read, Update, Delete) application forms the foundation of modern web development. When you combine Laravel’s elegant framework with MongoDB’s flexible NoSQL database, you get a powerful full-stack solution that handles complex data structures with ease.
This comprehensive guide walks you through creating a complete CRUD application using MongoDB and Laravel. You’ll learn how to set up the development environment, design a NoSQL schema, implement all CRUD operations, and deploy your finished application. Whether you’re new to NoSQL databases or looking to expand your Laravel skills, this tutorial provides practical, step-by-step instructions that work in real-world projects.
Introduction
What is CRUD and Why It Matters in Web Development
CRUD operations represent the four fundamental functions of persistent storage: Create, Read, Update, and Delete. These operations form the backbone of virtually every web application, from simple blogs to complex enterprise systems. Understanding CRUD is essential because it defines how users interact with your application’s data.
Every time a user registers an account (Create), views a profile (Read), edits their information (Update), or removes a post (Delete), they’re performing CRUD operations. Mastering these operations with MongoDB and Laravel gives you the skills to build dynamic, data-driven applications that scale with your users’ needs.
Benefits of Combining MongoDB and Laravel for Full-Stack Development
Flexible Data Structure: MongoDB’s document-based storage allows you to store complex, nested data without rigid table schemas. This flexibility is perfect for modern applications that handle varied data types.
Rapid Development: Laravel’s expressive syntax combined with MongoDB’s schema-less design means you can prototype and iterate faster than traditional SQL-based approaches.
Scalability: MongoDB’s horizontal scaling capabilities pair well with Laravel’s queue system and caching mechanisms, creating applications that grow with your user base.
Modern Development Patterns: Both technologies embrace modern development practices like RESTful APIs, MVC architecture, and test-driven development.
Rich Query Capabilities: MongoDB’s aggregation framework provides powerful data analysis features, while Laravel’s Eloquent ORM makes complex queries readable and maintainable.
Setting Up the Development Environment
Installing Laravel and Composer for Backend Setup
Before building your CRUD application, ensure you have the necessary tools installed. Laravel requires PHP 8.1+ and Composer for dependency management.
Install Composer (if not already installed):
For macOS and Linux:
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
For Windows, download the installer from getcomposer.org.
Verify PHP version:
php --version
Install Laravel globally:
composer global require laravel/installer
Add Composer’s global bin directory to your PATH:
export PATH="$HOME/.composer/vendor/bin:$PATH"
Installing MongoDB and Setting Up the Database Locally
Install MongoDB Community Edition:
For Ubuntu/Debian:
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
For macOS using Homebrew:
brew tap mongodb/brew
brew install [email protected]
For Windows, download the installer from mongodb.com.
Start MongoDB service:
# Linux/macOS
sudo systemctl start mongod
# macOS with Homebrew
brew services start mongodb/brew/mongodb-community
# Windows
net start MongoDB
Verify MongoDB installation:
mongosh
This opens the MongoDB shell. Exit with exit
.
Using Laravel MongoDB Package for Seamless Integration
Laravel doesn’t include MongoDB support by default, but the jenssegers/mongodb
package provides excellent Eloquent integration for MongoDB.
Install the MongoDB PHP extension:
# Ubuntu/Debian
sudo apt-get install php-mongodb
# macOS
brew install php-mongodb
# Windows - download from https://pecl.php.net/package/mongodb
Install the Laravel MongoDB package:
composer require jenssegers/mongodb
Creating the Laravel Project
Starting a New Laravel Project with Artisan
Create your new Laravel project:
laravel new laravel-mongodb-crud
cd laravel-mongodb-crud
Alternatively, using Composer:
composer create-project laravel/laravel laravel-mongodb-crud
cd laravel-mongodb-crud
Install the MongoDB package:
composer require jenssegers/mongodb
Publish the MongoDB configuration (optional):
php artisan vendor:publish --provider="Jenssegers\Mongodb\MongodbServiceProvider"
Configuring MongoDB Connection in Laravel .env and config/database.php
Update your .env
file:
DB_CONNECTION=mongodb
DB_HOST=127.0.0.1
DB_PORT=27017
DB_DATABASE=laravel_crud
DB_USERNAME=
DB_PASSWORD=
Configure the database connection in config/database.php
:
Add this to the connections
array:
'mongodb' => [
'driver' => 'mongodb',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 27017),
'database' => env('DB_DATABASE', 'laravel_crud'),
'username' => env('DB_USERNAME', ''),
'password' => env('DB_PASSWORD', ''),
'options' => [
// feel free to add more options
],
],
Update the default database connection:
'default' => env('DB_CONNECTION', 'mongodb'),
Test the connection:
php artisan tinker
In tinker:
DB::connection()->getMongoDB()->listCollections();
Designing the Database Schema
Understanding MongoDB Collections vs SQL Tables
Unlike SQL databases with rigid table structures, MongoDB stores data in flexible documents within collections. Each document is a JSON-like object that can have different fields and structures.
SQL Table Structure:
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255),
created_at TIMESTAMP
);
MongoDB Collection Structure:
{
"_id": ObjectId("..."),
"name": "John Doe",
"email": "[email protected]",
"profile": {
"age": 30,
"interests": ["coding", "gaming"],
"address": {
"city": "New York",
"country": "USA"
}
},
"created_at": ISODate("...")
}
Planning the Schema for CRUD Operations
For this tutorial, we’ll build a Task Management System with the following structure:
Tasks Collection Schema:
_id
: Unique identifier (automatically generated)title
: Task title (string, required)description
: Task description (text)status
: Task status (enum: pending, in_progress, completed)priority
: Priority level (enum: low, medium, high)due_date
: Due date (date, optional)tags
: Array of tags (array of strings)created_at
: Creation timestampupdated_at
: Last update timestamp
Categories Collection Schema:
_id
: Unique identifiername
: Category name (string, required)color
: Display color (string)description
: Category description (text)
Creating MongoDB Collections Using Laravel
MongoDB collections are created automatically when you insert the first document. However, you can create them explicitly using Laravel migrations:
Create a migration for tasks:
php artisan make:migration create_tasks_collection
Edit the migration file:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTasksCollection extends Migration
{
public function up()
{
// MongoDB collections are created automatically
// This migration serves as documentation
Schema::connection('mongodb')->create('tasks', function (Blueprint $collection) {
$collection->index('title');
$collection->index('status');
$collection->index('due_date');
});
}
public function down()
{
Schema::connection('mongodb')->drop('tasks');
}
}
Run the migration:
php artisan migrate
Building the Model Layer
Creating Eloquent Models for MongoDB Collections
Laravel’s MongoDB package extends Eloquent to work seamlessly with MongoDB collections.
Create the Task model:
php artisan make:model Task
Edit app/Models/Task.php
:
<?php
namespace App\Models;
use Jenssegers\Mongodb\Eloquent\Model;
use Carbon\Carbon;
class Task extends Model
{
protected $connection = 'mongodb';
protected $collection = 'tasks';
protected $fillable = [
'title',
'description',
'status',
'priority',
'due_date',
'tags',
];
protected $casts = [
'due_date' => 'datetime',
'tags' => 'array',
];
// Define status constants
const STATUS_PENDING = 'pending';
const STATUS_IN_PROGRESS = 'in_progress';
const STATUS_COMPLETED = 'completed';
// Define priority constants
const PRIORITY_LOW = 'low';
const PRIORITY_MEDIUM = 'medium';
const PRIORITY_HIGH = 'high';
// Accessor for formatted due date
public function getFormattedDueDateAttribute()
{
return $this->due_date ? $this->due_date->format('M d, Y') : null;
}
// Scope for filtering by status
public function scopeByStatus($query, $status)
{
return $query->where('status', $status);
}
// Scope for overdue tasks
public function scopeOverdue($query)
{
return $query->where('due_date', '<', Carbon::now())
->where('status', '!=', self::STATUS_COMPLETED);
}
// Check if task is overdue
public function isOverdue()
{
return $this->due_date &&
$this->due_date->isPast() &&
$this->status !== self::STATUS_COMPLETED;
}
}
Create the Category model:
php artisan make:model Category
Edit app/Models/Category.php
:
<?php
namespace App\Models;
use Jenssegers\Mongodb\Eloquent\Model;
class Category extends Model
{
protected $connection = 'mongodb';
protected $collection = 'categories';
protected $fillable = [
'name',
'color',
'description',
];
// Relationship with tasks (if you add category_id to tasks)
public function tasks()
{
return $this->hasMany(Task::class, 'category_id');
}
}
Using Laravel MongoDB Extensions to Interact with NoSQL
The MongoDB package provides several unique features for NoSQL operations:
Working with embedded documents:
// Create a task with embedded metadata
Task::create([
'title' => 'Learn MongoDB',
'description' => 'Study MongoDB with Laravel',
'status' => Task::STATUS_PENDING,
'metadata' => [
'source' => 'tutorial',
'difficulty' => 'intermediate',
'estimated_hours' => 4
]
]);
// Query embedded documents
$tasks = Task::where('metadata.difficulty', 'intermediate')->get();
Array operations:
// Add tags to a task
$task = Task::find($id);
$task->push('tags', 'urgent');
// Remove a tag
$task->pull('tags', 'urgent');
// Add multiple tags
$task->push('tags', ['important', 'deadline'], true);
Setting Up Routes and Controllers
Defining RESTful Routes for CRUD Functionality
Laravel’s resource routes provide a clean way to define all CRUD operations.
Add routes to routes/web.php
:
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TaskController;
use App\Http\Controllers\CategoryController;
Route::get('/', function () {
return redirect()->route('tasks.index');
});
// Task routes
Route::resource('tasks', TaskController::class);
// Category routes
Route::resource('categories', CategoryController::class);
// API routes for AJAX operations
Route::prefix('api')->group(function () {
Route::patch('tasks/{task}/status', [TaskController::class, 'updateStatus']);
Route::get('tasks/search', [TaskController::class, 'search']);
});
View all routes:
php artisan route:list
Creating Controllers and Handling HTTP Requests in Laravel
Create the TaskController:
php artisan make:controller TaskController --resource
Edit app/Http/Controllers/TaskController.php
:
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use App\Models\Category;
use Illuminate\Http\Request;
use Carbon\Carbon;
class TaskController extends Controller
{
public function index(Request $request)
{
$query = Task::query();
// Filter by status
if ($request->has('status') && $request->status !== '') {
$query->where('status', $request->status);
}
// Filter by priority
if ($request->has('priority') && $request->priority !== '') {
$query->where('priority', $request->priority);
}
// Search in title and description
if ($request->has('search') && $request->search !== '') {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
$tasks = $query->orderBy('created_at', 'desc')->paginate(10);
return view('tasks.index', compact('tasks'));
}
public function create()
{
$categories = Category::all();
return view('tasks.create', compact('categories'));
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'priority' => 'required|in:low,medium,high',
'due_date' => 'nullable|date|after:today',
'tags' => 'nullable|string',
]);
// Process tags
if ($validated['tags']) {
$validated['tags'] = array_map('trim', explode(',', $validated['tags']));
} else {
$validated['tags'] = [];
}
// Convert due_date to Carbon instance
if ($validated['due_date']) {
$validated['due_date'] = Carbon::parse($validated['due_date']);
}
Task::create($validated);
return redirect()->route('tasks.index')
->with('success', 'Task created successfully!');
}
public function show(Task $task)
{
return view('tasks.show', compact('task'));
}
public function edit(Task $task)
{
$categories = Category::all();
return view('tasks.edit', compact('task', 'categories'));
}
public function update(Request $request, Task $task)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'priority' => 'required|in:low,medium,high',
'due_date' => 'nullable|date',
'tags' => 'nullable|string',
]);
// Process tags
if ($validated['tags']) {
$validated['tags'] = array_map('trim', explode(',', $validated['tags']));
} else {
$validated['tags'] = [];
}
// Convert due_date to Carbon instance
if ($validated['due_date']) {
$validated['due_date'] = Carbon::parse($validated['due_date']);
}
$task->update($validated);
return redirect()->route('tasks.index')
->with('success', 'Task updated successfully!');
}
public function destroy(Task $task)
{
$task->delete();
return redirect()->route('tasks.index')
->with('success', 'Task deleted successfully!');
}
// AJAX method to update task status
public function updateStatus(Request $request, Task $task)
{
$validated = $request->validate([
'status' => 'required|in:pending,in_progress,completed'
]);
$task->update(['status' => $validated['status']]);
return response()->json([
'success' => true,
'message' => 'Status updated successfully'
]);
}
// AJAX search method
public function search(Request $request)
{
$search = $request->get('q');
$tasks = Task::where('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%")
->limit(10)
->get(['_id', 'title', 'status']);
return response()->json($tasks);
}
}
Creating the Views with Blade Templates
Setting Up Blade Templating Engine
Blade is Laravel’s powerful templating engine that comes built-in. It provides clean syntax for displaying data and creating reusable layouts.
Create the main layout file resources/views/layouts/app.blade.php
:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Task Manager') }} - @yield('title')</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom CSS -->
<style>
.task-card {
transition: transform 0.2s;
}
.task-card:hover {
transform: translateY(-2px);
}
.priority-high { border-left: 4px solid #dc3545; }
.priority-medium { border-left: 4px solid #ffc107; }
.priority-low { border-left: 4px solid #28a745; }
.status-pending { background-color: #fff3cd; }
.status-in_progress { background-color: #d1ecf1; }
.status-completed { background-color: #d4edda; }
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ route('tasks.index') }}">
<i class="fas fa-tasks"></i> Task Manager
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="{{ route('tasks.index') }}">Tasks</a>
<a class="nav-link" href="{{ route('categories.index') }}">Categories</a>
</div>
</div>
</nav>
<main class="container mt-4">
<!-- Flash Messages -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@yield('content')
</main>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
@stack('scripts')
</body>
</html>
Building Clean and Responsive UI with Bootstrap or Tailwind CSS
We’ll use Bootstrap 5 for responsive design and clean components.
Create the tasks index view resources/views/tasks/index.blade.php
:
@extends('layouts.app')
@section('title', 'All Tasks')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Tasks</h1>
<a href="{{ route('tasks.create') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> New Task
</a>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" action="{{ route('tasks.index') }}" class="row g-3">
<div class="col-md-3">
<label for="status" class="form-label">Status</label>
<select name="status" id="status" class="form-select">
<option value="">All Status</option>
<option value="pending" {{ request('status') === 'pending' ? 'selected' : '' }}>Pending</option>
<option value="in_progress" {{ request('status') === 'in_progress' ? 'selected' : '' }}>In Progress</option>
<option value="completed" {{ request('status') === 'completed' ? 'selected' : '' }}>Completed</option>
</select>
</div>
<div class="col-md-3">
<label for="priority" class="form-label">Priority</label>
<select name="priority" id="priority" class="form-select">
<option value="">All Priorities</option>
<option value="high" {{ request('priority') === 'high' ? 'selected' : '' }}>High</option>
<option value="medium" {{ request('priority') === 'medium' ? 'selected' : '' }}>Medium</option>
<option value="low" {{ request('priority') === 'low' ? 'selected' : '' }}>Low</option>
</select>
</div>
<div class="col-md-4">
<label for="search" class="form-label">Search</label>
<input type="text" name="search" id="search" class="form-control"
placeholder="Search tasks..." value="{{ request('search') }}">
</div>
<div class="col-md-2">
<label class="form-label"> </label>
<div class="d-grid">
<button type="submit" class="btn btn-outline-primary">Filter</button>
</div>
</div>
</form>
</div>
</div>
<!-- Tasks Grid -->
<div class="row">
@forelse($tasks as $task)
<div class="col-lg-4 col-md-6 mb-4">
<div class="card task-card h-100 priority-{{ $task->priority }}">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="badge bg-{{ $task->status === 'completed' ? 'success' : ($task->status === 'in_progress' ? 'info' : 'warning') }}">
{{ ucfirst(str_replace('_', ' ', $task->status)) }}
</span>
<span class="badge bg-{{ $task->priority === 'high' ? 'danger' : ($task->priority === 'medium' ? 'warning' : 'success') }}">
{{ ucfirst($task->priority) }}
</span>
</div>
<div class="card-body">
<h5 class="card-title">{{ $task->title }}</h5>
<p class="card-text">{{ Str::limit($task->description, 100) }}</p>
@if($task->due_date)
<p class="text-muted small">
<i class="fas fa-calendar"></i>
Due: {{ $task->formatted_due_date }}
@if($task->isOverdue())
<span class="text-danger">(Overdue)</span>
@endif
</p>
@endif
@if($task->tags && count($task->tags) > 0)
<div class="mb-2">
@foreach($task->tags as $tag)
<span class="badge bg-light text-dark me-1">{{ $tag }}</span>
@endforeach
</div>
@endif
</div>
<div class="card-footer">
<div class="btn-group w-100" role="group">
<a href="{{ route('tasks.show', $task) }}" class="btn btn-sm btn-outline-info">View</a>
<a href="{{ route('tasks.edit', $task) }}" class="btn btn-sm btn-outline-primary">Edit</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('Are you sure?')">Delete</button>
</form>
</div>
</div>
</div>
</div>
@empty
<div class="col-12">
<div class="text-center py-5">
<i class="fas fa-tasks fa-3x text-muted mb-3"></i>
<h3 class="text-muted">No tasks found</h3>
<p class="text-muted">Create your first task to get started!</p>
<a href="{{ route('tasks.create') }}" class="btn btn-primary">Create Task</a>
</div>
</div>
@endforelse
</div>
<!-- Pagination -->
@if($tasks->hasPages())
<div class="d-flex justify-content-center">
{{ $tasks->appends(request()->query())->links() }}
</div>
@endif
@endsection
Creating Reusable Layouts for CRUD Pages
Create the task form component resources/views/tasks/_form.blade.php
:
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="title" class="form-label">Title <span class="text-danger">*</span></label>
<input type="text" name="title" id="title" class="form-control @error('title') is-invalid @enderror"
value="{{ old('title', $task->title ?? '') }}" required>
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea name="description" id="description" rows="4"
class="form-control @error('description') is-invalid @enderror">{{ old('description', $task->description ?? '') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="status" class="form-label">Status <span class="text-danger">*</span></label>
<select name="status" id="status" class="form-select @error('status') is-invalid @enderror" required>
<option value="">Select Status</option>
<option value="pending" {{ old('status', $task->status ?? '') === 'pending' ? 'selected' : '' }}>Pending</option>
<option value="in_progress" {{ old('status', $task->status ?? '') === 'in_progress' ? 'selected' : '' }}>In Progress</option>
<option value="completed" {{ old('status', $task->status ?? '') === 'completed' ? 'selected' : '' }}>Completed</option>
</select>
@error('status')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="priority" class="form-label">Priority <span class="text-danger">*</span></label>
<select name="priority" id="priority" class="form-select @error('priority') is-invalid @enderror" required>
<option value="">Select Priority</option>
<option value="low" {{ old('priority', $task->priority ?? '') === 'low' ? 'selected' : '' }}>Low</option>
<option value="medium" {{ old('priority', $task->priority ?? '') === 'medium' ? 'selected' : '' }}>Medium</option>
<option value="high" {{ old('priority', $task->priority ?? '') === 'high' ? 'selected' : '' }}>High</option>
</select>
@error('priority')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="due_date" class="form-label">Due Date</label>
<input type="date" name="due_date" id="due_date"
class="form-control @error('due_date') is-invalid @enderror"
value="{{ old('due_date', $task->due_date ? $task->due_date->format('Y-m-d') : '') }}">
@error('due_date')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="tags" class="form-label">Tags</label>
<input type="text" name="tags" id="tags" class="form-control @error('tags') is-invalid @enderror"
placeholder="Enter tags separated by commas"
value="{{ old('tags', isset($task) && $task->tags ? implode(', ', $task->tags) : '') }}">
@error('tags')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<div class="form-text">Separate multiple tags with commas</div>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Tasks
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {{ isset($task) ? 'Update Task' : 'Create Task' }}
</button>
</div>
Implementing CRUD Operations
Creating a Record: Form Handling and Validation
Create the task creation view resources/views/tasks/create.blade.php
:
@extends('layouts.app')
@section('title', 'Create New Task')
@section('content')
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h3 class="mb-0">Create New Task</h3>
</div>
<div class="card-body">
<form action="{{ route('tasks.store') }}" method="POST">
@csrf
@include('tasks._form')
</form>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
// Set minimum date to today for due date
$('#due_date').attr('min', new Date().toISOString().split('T')[0]);
// Auto-capitalize first letter of title
$('#title').on('input', function() {
let value = $(this).val();
if (value.length === 1) {
$(this).val(value.toUpperCase());
}
});
});
</script>
@endpush
Reading Data: Displaying Records from MongoDB
Create the task detail view resources/views/tasks/show.blade.php
:
@extends('layouts.app')
@section('title', $task->title)
@section('content')
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="mb-0">{{ $task->title }}</h3>
<div>
<span class="badge bg-{{ $task->status === 'completed' ? 'success' : ($task->status === 'in_progress' ? 'info' : 'warning') }} me-2">
{{ ucfirst(str_replace('_', ' ', $task->status)) }}
</span>
<span class="badge bg-{{ $task->priority === 'high' ? 'danger' : ($task->priority === 'medium' ? 'warning' : 'success') }}">
{{ ucfirst($task->priority) }} Priority
</span>
</div>
</div>
<div class="card-body">
@if($task->description)
<div class="mb-4">
<h5>Description</h5>
<p class="text-muted">{{ $task->description }}</p>
</div>
@endif
<div class="row mb-4">
<div class="col-md-6">
<h6>Created</h6>
<p class="text-muted">
<i class="fas fa-calendar-plus"></i>
{{ $task->created_at->format('M d, Y \a\t g:i A') }}
</p>
</div>
@if($task->due_date)
<div class="col-md-6">
<h6>Due Date</h6>
<p class="text-muted {{ $task->isOverdue() ? 'text-danger' : '' }}">
<i class="fas fa-calendar-alt"></i>
{{ $task->formatted_due_date }}
@if($task->isOverdue())
<span class="badge bg-danger ms-2">Overdue</span>
@endif
</p>
</div>
@endif
</div>
@if($task->tags && count($task->tags) > 0)
<div class="mb-4">
<h6>Tags</h6>
@foreach($task->tags as $tag)
<span class="badge bg-light text-dark me-1 mb-1">
<i class="fas fa-tag"></i> {{ $tag }}
</span>
@endforeach
</div>
@endif
<div class="mb-4">
<h6>Last Updated</h6>
<p class="text-muted">
<i class="fas fa-clock"></i>
{{ $task->updated_at->diffForHumans() }}
</p>
</div>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between">
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Tasks
</a>
<div>
<a href="{{ route('tasks.edit', $task) }}" class="btn btn-primary me-2">
<i class="fas fa-edit"></i> Edit Task
</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger"
onclick="return confirm('Are you sure you want to delete this task?')">
<i class="fas fa-trash"></i> Delete
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Quick Status Update -->
<div class="card mt-3">
<div class="card-body">
<h6>Quick Status Update</h6>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-warning status-btn"
data-status="pending" data-task-id="{{ $task->_id }}">
Pending
</button>
<button type="button" class="btn btn-outline-info status-btn"
data-status="in_progress" data-task-id="{{ $task->_id }}">
In Progress
</button>
<button type="button" class="btn btn-outline-success status-btn"
data-status="completed" data-task-id="{{ $task->_id }}">
Completed
</button>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
// Highlight current status button
$('.status-btn[data-status="{{ $task->status }}"]').removeClass('btn-outline-warning btn-outline-info btn-outline-success')
.addClass('btn-warning btn-info btn-success'.split(' ')[{{ $task->status === 'pending' ? '0' : ($task->status === 'in_progress' ? '1' : '2') }}]);
// Handle status update
$('.status-btn').click(function() {
const status = $(this).data('status');
const taskId = $(this).data('task-id');
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
url: `/api/tasks/${taskId}/status`,
method: 'PATCH',
data: { status: status },
success: function(response) {
location.reload();
},
error: function(xhr) {
alert('Error updating status');
}
});
});
});
</script>
@endpush
Updating Records: Editable Forms with Pre-filled Data
Create the task edit view resources/views/tasks/edit.blade.php
:
@extends('layouts.app')
@section('title', 'Edit Task')
@section('content')
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h3 class="mb-0">Edit Task: {{ $task->title }}</h3>
</div>
<div class="card-body">
<form action="{{ route('tasks.update', $task) }}" method="POST">
@csrf
@method('PUT')
@include('tasks._form')
</form>
</div>
</div>
<!-- Task History (if implementing audit trail) -->
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">Task Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<small class="text-muted">Created: {{ $task->created_at->format('M d, Y \a\t g:i A') }}</small>
</div>
<div class="col-md-6">
<small class="text-muted">Last Updated: {{ $task->updated_at->format('M d, Y \a\t g:i A') }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
// Set minimum date to today for due date (only for future tasks)
const currentDueDate = $('#due_date').val();
if (!currentDueDate || new Date(currentDueDate) >= new Date()) {
$('#due_date').attr('min', new Date().toISOString().split('T')[0]);
}
// Warn about unsaved changes
let formChanged = false;
$('form input, form select, form textarea').on('change', function() {
formChanged = true;
});
$(window).on('beforeunload', function() {
if (formChanged) {
return 'You have unsaved changes. Are you sure you want to leave?';
}
});
$('form').on('submit', function() {
formChanged = false;
});
});
</script>
@endpush
Deleting Records: Soft Delete and Confirmation Dialogs
Add soft delete functionality to your Task model:
// Add to Task model
use Jenssegers\Mongodb\Eloquent\SoftDeletes;
class Task extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
// ... rest of your model code
}
Create a more sophisticated delete confirmation modal:
<!-- Add to your layout file before closing body tag -->
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Deletion</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this task?</p>
<p class="text-muted"><strong id="taskTitle"></strong></p>
<p class="text-danger small">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form id="deleteForm" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Delete Task</button>
</form>
</div>
</div>
</div>
</div>
<script>
// Handle delete confirmation
$(document).on('click', '.delete-task-btn', function(e) {
e.preventDefault();
const taskTitle = $(this).data('task-title');
const deleteUrl = $(this).data('delete-url');
$('#taskTitle').text(taskTitle);
$('#deleteForm').attr('action', deleteUrl);
$('#deleteModal').modal('show');
});
</script>
Update your task cards to use the modal:
<!-- Replace the delete button in your task cards -->
<button type="button" class="btn btn-sm btn-outline-danger delete-task-btn"
data-task-title="{{ $task->title }}"
data-delete-url="{{ route('tasks.destroy', $task) }}">
Delete
</button>
Testing the Application
Testing Each CRUD Operation in the Browser
Test the complete workflow:
- Create Operation: Navigate to
/tasks/create
and create a new task with various field combinations - Read Operation: Check the tasks index page and individual task detail pages
- Update Operation: Edit existing tasks and verify changes are saved
- Delete Operation: Test both the confirmation modal and actual deletion
Manual testing checklist:
- ✅ Create task with all fields filled
- ✅ Create task with only required fields
- ✅ Test form validation with invalid data
- ✅ View task list with different filters
- ✅ View individual task details
- ✅ Edit task and verify changes
- ✅ Update task status via AJAX
- ✅ Delete task with confirmation
- ✅ Test pagination with multiple tasks
- ✅ Test search functionality
Debugging Common Errors with Laravel and MongoDB
Common MongoDB Connection Issues:
// Debug MongoDB connection
php artisan tinker
// Test connection
DB::connection('mongodb')->getMongoDB()->ping();
// List collections
collect(DB::connection('mongodb')->getMongoDB()->listCollections())->pluck('name');
// Test model queries
App\Models\Task::count();
Debug Eloquent Queries:
// Enable query logging in AppServiceProvider
public function boot()
{
if (app()->environment('local')) {
DB::listen(function ($query) {
Log::info('MongoDB Query', [
'query' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time
]);
});
}
}
Common Error Solutions:
Error | Solution |
Class 'MongoDB\Driver\Manager' not found | Install PHP MongoDB extension |
Connection refused to 127.0.0.1:27017 | Start MongoDB service |
Collection not found | Collections are created automatically on first insert |
Validation failed | Check model fillable properties and validation rules |
Writing Feature Tests Using Laravel’s Testing Tools
Create a feature test for task CRUD operations:
php artisan make:test TaskCrudTest
Edit tests/Feature/TaskCrudTest.php
:
<?php
namespace Tests\Feature;
use App\Models\Task;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use Carbon\Carbon;
class TaskCrudTest extends TestCase
{
use RefreshDatabase, WithFaker;
public function test_can_create_task()
{
$taskData = [
'title' => 'Test Task',
'description' => 'This is a test task',
'status' => 'pending',
'priority' => 'medium',
'due_date' => Carbon::tomorrow()->format('Y-m-d'),
'tags' => 'test, laravel'
];
$response = $this->post(route('tasks.store'), $taskData);
$response->assertRedirect(route('tasks.index'));
$this->assertDatabaseHas('tasks', [
'title' => 'Test Task',
'status' => 'pending'
]);
}
public function test_can_read_tasks()
{
$task = Task::factory()->create();
$response = $this->get(route('tasks.index'));
$response->assertStatus(200);
$response->assertSee($task->title);
}
public function test_can_show_individual_task()
{
$task = Task::factory()->create();
$response = $this->get(route('tasks.show', $task));
$response->assertStatus(200);
$response->assertSee($task->title);
$response->assertSee($task->description);
}
public function test_can_update_task()
{
$task = Task::factory()->create();
$updateData = [
'title' => 'Updated Task Title',
'description' => $task->description,
'status' => 'completed',
'priority' => $task->priority,
'tags' => ''
];
$response = $this->put(route('tasks.update', $task), $updateData);
$response->assertRedirect(route('tasks.index'));
$this->assertDatabaseHas('tasks', [
'_id' => $task->_id,
'title' => 'Updated Task Title',
'status' => 'completed'
]);
}
public function test_can_delete_task()
{
$task = Task::factory()->create();
$response = $this->delete(route('tasks.destroy', $task));
$response->assertRedirect(route('tasks.index'));
$this->assertSoftDeleted('tasks', ['_id' => $task->_id]);
}
public function test_task_validation_works()
{
// Test required fields
$response = $this->post(route('tasks.store'), []);
$response->assertSessionHasErrors(['title', 'status', 'priority']);
}
public function test_can_filter_tasks_by_status()
{
Task::factory()->create(['status' => 'pending', 'title' => 'Pending Task']);
Task::factory()->create(['status' => 'completed', 'title' => 'Completed Task']);
$response = $this->get(route('tasks.index', ['status' => 'pending']));
$response->assertStatus(200);
$response->assertSee('Pending Task');
$response->assertDontSee('Completed Task');
}
public function test_can_search_tasks()
{
Task::factory()->create(['title' => 'Laravel MongoDB Tutorial']);
Task::factory()->create(['title' => 'Vue.js Component']);
$response = $this->get(route('tasks.index', ['search' => 'Laravel']));
$response->assertStatus(200);
$response->assertSee('Laravel MongoDB Tutorial');
$response->assertDontSee('Vue.js Component');
}
}
Create a Task factory for testing:
php artisan make:factory TaskFactory
Edit database/factories/TaskFactory.php
:
<?php
namespace Database\Factories;
use App\Models\Task;
use Illuminate\Database\Eloquent\Factories\Factory;
use Carbon\Carbon;
class TaskFactory extends Factory
{
protected $model = Task::class;
public function definition()
{
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->paragraph(),
'status' => $this->faker->randomElement(['pending', 'in_progress', 'completed']),
'priority' => $this->faker->randomElement(['low', 'medium', 'high']),
'due_date' => $this->faker->optional()->dateTimeBetween('now', '+30 days'),
'tags' => $this->faker->optional()->randomElements(['urgent', 'important', 'work', 'personal'], 2),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}
Run the tests:
php artisan test --filter TaskCrudTest
Deploying the Full-Stack App
Preparing the App for Production
Environment Configuration:
Create a production .env
file:
APP_NAME="Task Manager"
APP_ENV=production
APP_KEY=base64:your-generated-key-here
APP_DEBUG=false
APP_URL=https://yourdomain.com
LOG_CHANNEL=stack
LOG_LEVEL=error
DB_CONNECTION=mongodb
DB_HOST=your-mongodb-host
DB_PORT=27017
DB_DATABASE=your_production_db
DB_USERNAME=your_username
DB_PASSWORD=your_secure_password
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Optimize for Production:
# Clear and cache configurations
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Install production dependencies only
composer install --optimize-autoloader --no-dev
# Generate app key (if needed)
php artisan key:generate
Choosing Hosting for Laravel and MongoDB Projects
Recommended Hosting Options:
Provider | Laravel Support | MongoDB Support | Price Range |
DigitalOcean | ✅ Excellent | ✅ Managed MongoDB | $10-50/month |
AWS | ✅ Elastic Beanstalk | ✅ DocumentDB | $20-100/month |
Heroku | ✅ Native Support | ✅ MongoDB Atlas Add-on | $7-25/month |
Vultr | ✅ Cloud Compute | ✅ Self-managed | $5-30/month |
Docker Deployment Option:
Create a Dockerfile
:
FROM php:8.2-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip
# Install PHP extensions
RUN docker-php-ext-install pdo pdo_mysql mbstring exif pcntl bcmath gd
RUN pecl install mongodb && docker-php-ext-enable mongodb
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www
# Copy application files
COPY . /var/www
# Install dependencies
RUN composer install --optimize-autoloader --no-dev
# Set permissions
RUN chown -R www-data:www-data /var/www
RUN chmod -R 755 /var/www/storage
EXPOSE 9000
CMD ["php-fpm"]
Create docker-compose.yml
:
version: '3.8'
services:
app:
build: .
container_name: laravel-app
volumes:
- ./:/var/www
networks:
- app-network
depends_on:
- mongodb
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./:/var/www
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- app-network
depends_on:
- app
mongodb:
image: mongo:6.0
container_name: mongodb
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
MONGO_INITDB_DATABASE: laravel_crud
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mongodb_data:
Setting Up Environment Variables and Final Checks
Pre-deployment Checklist:
- ✅ Environment variables configured
- ✅ Database connection tested
- ✅ All dependencies installed
- ✅ Caches cleared and rebuilt
- ✅ SSL certificates configured
- ✅ Domain DNS pointing to server
- ✅ Backup strategy implemented
- ✅ Monitoring tools configured
Health Check Endpoint:
Add a health check route to monitor your application:
// routes/web.php
Route::get('/health', function () {
try {
// Test database connection
$tasksCount = App\Models\Task::count();
return response()->json([
'status' => 'healthy',
'database' => 'connected',
'tasks_count' => $tasksCount,
'timestamp' => now()->toISOString()
]);
} catch (Exception $e) {
return response()->json([
'status' => 'unhealthy',
'error' => $e->getMessage(),
'timestamp' => now()->toISOString()
], 500);
}
});
Best Practices and Optimization Tips
Securing User Input and Preventing Injection Attacks
Input Validation Best Practices:
// Create a Form Request for better validation
php artisan make:request StoreTaskRequest
// app/Http/Requests/StoreTaskRequest.php
class StoreTaskRequest extends FormRequest
{
public function authorize()
{
return true; // Implement your authorization logic
}
public function rules()
{
return [
'title' => ['required', 'string', 'max:255', 'regex:/^[a-zA-Z0-9\s\-_\.]+$/'],
'description' => ['nullable', 'string', 'max:2000'],
'status' => ['required', Rule::in(['pending', 'in_progress', 'completed'])],
'priority' => ['required', Rule::in(['low', 'medium', 'high'])],
'due_date' => ['nullable', 'date', 'after:yesterday'],
'tags' => ['nullable', 'string', 'max:500'],
];
}
public function messages()
{
return [
'title.regex' => 'Title contains invalid characters.',
'due_date.after' => 'Due date must be in the future.',
];
}
protected function prepareForValidation()
{
// Sanitize input
$this->merge([
'title' => strip_tags($this->title),
'description' => strip_tags($this->description),
]);
}
}
Use the Form Request in your controller:
public function store(StoreTaskRequest $request)
{
$validated = $request->validated();
// Process tags safely
if ($validated['tags']) {
$validated['tags'] = array_map(function($tag) {
return trim(strip_tags($tag));
}, explode(',', $validated['tags']));
}
Task::create($validated);
return redirect()->route('tasks.index')
->with('success', 'Task created successfully!');
}
Optimizing MongoDB Queries for Performance
Index Strategy:
// Create indexes for better query performance
// In your migration or MongoDB shell
// Single field indexes
db.tasks.createIndex({ "status": 1 })
db.tasks.createIndex({ "priority": 1 })
db.tasks.createIndex({ "due_date": 1 })
db.tasks.createIndex({ "created_at": -1 })
// Compound indexes for common query patterns
db.tasks.createIndex({ "status": 1, "priority": 1 })
db.tasks.createIndex({ "status": 1, "due_date": 1 })
// Text index for search functionality
db.tasks.createIndex({
"title": "text",
"description": "text"
}, {
weights: {
title: 10,
description: 5
}
})
Optimize Eloquent Queries:
// Use select() to limit fields
$tasks = Task::select(['title', 'status', 'priority', 'due_date'])
->where('status', 'pending')
->limit(50)
->get();
// Use pagination for large datasets
$tasks = Task::where('status', 'pending')
->orderBy('created_at', 'desc')
->paginate(20);
// Eager loading for relationships (if implemented)
$tasks = Task::with('category')
->where('status', 'pending')
->get();
// Use aggregation for complex queries
$statusCounts = Task::raw(function($collection) {
return $collection->aggregate([
[
'$group' => [
'_id' => '$status',
'count' => ['$sum' => 1]
]
]
]);
});
Using Laravel’s Caching and Error Logging Features
Implement Query Caching:
// Cache frequently accessed data
public function index(Request $request)
{
$cacheKey = 'tasks_' . md5(serialize($request->all()));
$tasks = Cache::remember($cacheKey, 300, function () use ($request) {
$query = Task::query();
// Apply filters...
return $query->paginate(10);
});
return view('tasks.index', compact('tasks'));
}
// Clear cache when data changes
public function store(StoreTaskRequest $request)
{
$task = Task::create($request->validated());
// Clear related caches
Cache::tags(['tasks'])->flush();
return redirect()->route('tasks.index')
->with('success', 'Task created successfully!');
}
Enhanced Error Logging:
// In your controller methods
try {
$task = Task::create($validated);
Log::info('Task created successfully', [
'task_id' => $task->_id,
'user_ip' => request()->ip(),
'user_agent' => request()->userAgent()
]);
} catch (Exception $e) {
Log::error('Failed to create task', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'input' => $request->all(),
'user_ip' => request()->ip()
]);
return back()->withErrors(['error' => 'Failed to create task. Please try again.'])
->withInput();
}
Custom Error Handler for MongoDB Operations:
// Create app/Exceptions/MongoDBException.php
class MongoDBException extends Exception
{
public function report()
{
Log::channel('mongodb')->error('MongoDB Operation Failed', [
'message' => $this->getMessage(),
'file' => $this->getFile(),
'line' => $this->getLine()
]);
}
public function render($request)
{
if ($request->expectsJson()) {
return response()->json([
'error' => 'Database operation failed',
'message' => 'Please try again later'
], 500);
}
return back()->withErrors(['error' => 'Something went wrong. Please try again.']);
}
}
Performance Monitoring Middleware:
// Create app/Http/Middleware/MonitorPerformance.php
php artisan make:middleware MonitorPerformance
class MonitorPerformance
{
public function handle($request, Closure $next)
{
$startTime = microtime(true);
$startMemory = memory_get_usage(true);
$response = $next($request);
$endTime = microtime(true);
$endMemory = memory_get_usage(true);
$executionTime = ($endTime - $startTime) * 1000; // Convert to milliseconds
$memoryUsed = $endMemory - $startMemory;
if ($executionTime > 1000) { // Log slow requests (>1 second)
Log::warning('Slow request detected', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'execution_time' => $executionTime . 'ms',
'memory_used' => $this->formatBytes($memoryUsed),
'user_agent' => $request->userAgent()
]);
}
return $response;
}
private function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}
Conclusion
Building a full-stack CRUD application with MongoDB and Laravel demonstrates the power of combining modern NoSQL databases with elegant PHP frameworks. You’ve successfully created a complete task management system that handles all fundamental database operations while maintaining clean, maintainable code.
Throughout this tutorial, you’ve mastered essential skills including MongoDB integration with Laravel, implementing RESTful routes and controllers, creating responsive Blade templates, and deploying production-ready applications. The combination of Laravel’s expressive syntax and MongoDB’s flexible document structure provides a solid foundation for building scalable web applications.
Your task management application now includes advanced features like real-time status updates, comprehensive search and filtering, proper validation and error handling, and professional-grade security measures. These patterns and techniques apply to virtually any web application you’ll build in the future.
Recap of What You Built
Core CRUD Functionality:
- ✅ Create tasks with rich form validation
- ✅ Read tasks with advanced filtering and search
- ✅ Update tasks with pre-filled forms and AJAX status updates
- ✅ Delete tasks with confirmation modals and soft deletes
Advanced Features:
- ✅ MongoDB integration with Laravel Eloquent
- ✅ Responsive Bootstrap UI with custom styling
- ✅ Real-time status updates using AJAX
- ✅ Comprehensive form validation and error handling
- ✅ Search and filtering capabilities
- ✅ Performance optimization with caching and indexing
- ✅ Production deployment configuration
- ✅ Comprehensive testing suite
Professional Development Practices:
- ✅ MVC architecture implementation
- ✅ RESTful API design
- ✅ Input sanitization and security measures
- ✅ Error logging and monitoring
- ✅ Code organization and reusability
- ✅ Database optimization strategies
Encouragement to Expand Features Like Auth, Pagination, and Search
Your foundation is solid, but there’s so much more you can build on top of it. Consider adding these features to create an even more robust application:
User Authentication and Authorization:
// Install Laravel Breeze for authentication
composer require laravel/breeze --dev
php artisan breeze:install
npm install && npm run dev
php artisan migrate
// Add user relationship to tasks
// In Task model
public function user()
{
return $this->belongsTo(User::class);
}
// In TaskController, scope tasks to authenticated user
public function index()
{
$tasks = auth()->user()->tasks()->paginate(10);
return view('tasks.index', compact('tasks'));
}
Advanced Search with MongoDB Text Indexes:
// Enhanced search functionality
public function search(Request $request)
{
$query = $request->get('q');
$tasks = Task::raw(function($collection) use ($query) {
return $collection->find([
'$text' => ['$search' => $query]
], [
'score' => ['$meta' => 'textScore']
])->sort([
'score' => ['$meta' => 'textScore']
]);
});
return response()->json($tasks);
}
Real-time Notifications with WebSockets:
// Install Laravel WebSockets
composer require pusher/pusher-php-server
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider"
// Create notification event
php artisan make:event TaskStatusUpdated
class TaskStatusUpdated implements ShouldBroadcast
{
public $task;
public function __construct(Task $task)
{
$this->task = $task;
}
public function broadcastOn()
{
return new Channel('tasks');
}
}
API Development for Mobile Apps:
// Create API controllers
php artisan make:controller Api/TaskController --api
// Add API routes
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('tasks', Api\TaskController::class);
});
// Return JSON responses
public function index()
{
$tasks = Task::paginate(20);
return TaskResource::collection($tasks);
}
Next Steps for Learning Full-Stack Development with Laravel and MongoDB
Immediate Next Steps:
- Deploy Your Application: Get your task manager live on a hosting platform
- Add User Authentication: Implement user registration and login functionality
- Create an API: Build RESTful APIs for mobile app integration
- Implement Real-time Features: Add WebSocket support for live updates
- Performance Testing: Use tools like Laravel Telescope to monitor performance
Advanced Learning Path:
Database Mastery:
- Study MongoDB aggregation pipelines for complex data analysis
- Learn about MongoDB sharding and replica sets for scalability
- Explore time-series collections for analytics data
- Master database indexing strategies for optimal performance
Laravel Ecosystem:
- Laravel Nova for admin panels
- Laravel Horizon for queue monitoring
- Laravel Telescope for debugging
- Laravel Sanctum for API authentication
Frontend Enhancement:
- Learn Vue.js or React for dynamic frontend interfaces
- Implement Progressive Web App (PWA) features
- Add offline functionality with service workers
- Master responsive design patterns
DevOps and Deployment:
- Container orchestration with Docker and Kubernetes
- Continuous Integration/Continuous Deployment (CI/CD) pipelines
- Infrastructure as Code with Terraform
- Monitoring and alerting with tools like New Relic or DataDog
Recommended Resources:
- Documentation: Laravel Official Docs and MongoDB Manual
- Video Courses: Laracasts for Laravel mastery
- Community: Laravel News and r/laravel
- Books: “Laravel: Up & Running” by Matt Stauffer
- Conferences: Laracon for networking and learning
The skills you’ve developed building this CRUD application with MongoDB and Laravel form the foundation of modern web development. You’re now equipped to tackle complex, real-world projects and continue growing as a full-stack developer.
Your journey in full-stack development has just begun. The patterns, techniques, and best practices you’ve learned here will serve you well as you tackle increasingly complex projects. Keep building, keep learning, and most importantly, keep shipping great applications that solve real problems for real people.
Remember: every expert was once a beginner who refused to give up. Your next project awaits - go build something amazing!