Laravel is a PHP framework: it gives you routing, a database layer, templating, validation, authentication and a queue system so you are not writing those from scratch. This walks through installing it and building one page that actually does something — a list of items you can add to. By the end you will have touched every part of the framework you use daily.
Before you install
Laravel 13, released 17 March 2026, needs PHP 8.3 or newer (8.3 to 8.5 are supported). Check what you have with php -v. If you are on XAMPP, the bundled PHP is often older than you think, and this is the single most common reason a fresh install fails on a Windows machine.
You also need Composer, PHP's package manager, from getcomposer.org. And you need a database — MySQL or MariaDB is what almost every Bangladeshi host gives you, and both work without changes.
One note worth having early: if your hosting is cPanel shared hosting, check which PHP versions MultiPHP Manager offers before you start. A host stuck on PHP 8.1 cannot run Laravel 13 at all, and finding that out after you have built the thing is a bad afternoon.
Create the project
composer create-project laravel/laravel myapp
cd myapp
php artisan serve
That last command starts a development server, usually at http://127.0.0.1:8000. Open it and you should see the Laravel welcome page. If you prefer, Laravel Herd (Mac and Windows) or Sail (Docker) will manage PHP for you, which sidesteps the version problem above.
What the folders are for
A new project looks overwhelming. In practice you will spend nearly all your time in four places:
routes/web.php— every URL your site answers, and what handles it.app/Models/— one class per database table.app/Http/Controllers/— the code that runs for a request.resources/views/— your Blade templates, the HTML.
The rest, briefly: database/migrations/ holds your schema history, config/ holds settings read from .env, public/ is the only folder the web server should expose, and storage/ holds logs, caches and uploaded files. The .env file at the root holds your database credentials and app key; it is deliberately not in version control, and copying someone else's is how secrets leak.
Your first route
Open routes/web.php and add:
Route::get('/hello', function () {
return 'It works';
});
Visit /hello. Returning a string is fine for a test, but real routes point at a controller:
php artisan make:controller TaskController
// routes/web.php
use App\Http\Controllers\TaskController;
Route::get('/tasks', [TaskController::class, 'index'])->name('tasks.index');
Route::post('/tasks', [TaskController::class, 'store'])->name('tasks.store');
Naming routes matters more than it looks: route('tasks.index') in a template keeps working when you later change the URL.
A table and a model
Set your database in .env first:
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=
Then create the model and its migration in one command:
php artisan make:model Task -m
Open the new file in database/migrations/ and describe the table:
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->boolean('done')->default(false);
$table->timestamps();
});
}
php artisan migrate
The table now exists. A migration is a record of a schema change that any machine can replay, which is why you never edit the database by hand once a project has migrations — your change would exist on your laptop and nowhere else.
In app/Models/Task.php, allow the fields you intend to accept from a form:
protected $fillable = ['title', 'done'];
That list is a security control called mass-assignment protection. Without it, someone could post an extra field and set a column you never meant to expose.
The controller
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index()
{
$tasks = Task::latest()->get();
return view('tasks.index', ['tasks' => $tasks]);
}
public function store(Request $request)
{
$data = $request->validate([
'title' => 'required|string|max:120',
]);
Task::create($data);
return redirect()->route('tasks.index')->with('ok', 'Task added.');
}
}
Two things are doing a lot of work here. validate() checks the input and, if it fails, sends the user back with the errors and their old input already populated — you write no error-handling code at all. And Task::create() is Eloquent, Laravel's database layer: no SQL, and the values are bound safely, so this is not vulnerable to injection.
The view
Create resources/views/tasks/index.blade.php:
<h1>Tasks</h1>
@if (session('ok'))
<p>{{ session('ok') }}</p>
@endif
<form method="post" action="{{ route('tasks.store') }}">
@csrf
<input name="title" value="{{ old('title') }}">
<button>Add</button>
@error('title') <span>{{ $message }}</span> @enderror
</form>
<ul>
@forelse ($tasks as $task)
<li>{{ $task->title }}</li>
@empty
<li>Nothing yet.</li>
@endforelse
</ul>
Blade compiles to plain PHP. {{ }} escapes its output, so a task title containing HTML is printed, not executed — that is your cross-site-scripting protection, and you get it by default. @csrf adds the hidden token that proves the form came from your own site; leave it out and the post is rejected with a 419.
Visit /tasks and add one. That is a full request cycle: route, controller, validation, model, database, view.
What to learn next, in order
- Relationships.
hasManyandbelongsTo, then eager loading withwith()— because the first performance problem you hit will be the N+1 query. - Authentication. Do not write it yourself. A starter kit gives you registration, login and password reset in one command.
- Form requests. Move validation rules out of the controller into their own class once you have more than a couple.
- Blade layouts. One parent template with
@extendsand@section, so the header lives in one file. - Testing.
php artisan testwith a factory to generate rows. Even three tests will catch the refactor that breaks your form. - Queues. Once something slow appears — email, image resizing, an external API — move it off the request.
Deploying, and the three things that go wrong
Almost every first deployment to shared hosting fails in one of the same ways:
- The document root. The web server must point at
public/, never the project root. If it points at the root, your.envfile becomes downloadable, which hands over your database password. - Permissions. Exactly two directories need to be writable by whichever user the web server runs as:
storage/andbootstrap/cache/. Get this wrong and Laravel cannot even open its own log file, so the symptom is silence rather than a useful message. Change the owner of those two; opening up the whole project is a security hole dressed as a fix. - A blank page on the server that works locally. That combination almost always means debug output is switched off in production, which is correct, and the real error went to the log. Open the newest entry in
storage/logs/and read the first line of the trace before changing anything.
Run php artisan config:cache and route:cache on the server for a real speed gain — and remember to clear them after changing config, because a stale cache reproduces a bug you have already fixed. If you hit errors beyond these, our piece on five common Laravel errors covers the next ones you are likely to meet.

