Getting Started with Laravel: A Beginner's Guide
If you've written a PHP app by hand, you know how it goes. You start with a few files, then you're copying the same database connection code everywhere, building your own login system, and escaping output in some templates but forgetting in others. By the time you add password resets, you've written half a framework, and not a very good one.
Laravel takes care of that groundwork so you can focus on the app itself. This post walks through installing it, how the folders are laid out, and the basics of routing, controllers, Eloquent, Blade, validation and authentication. It ends with a few problems that trip up almost everyone on their first deploy.
It's one of the most popular PHP frameworks on GitHub, the docs are good, and when you get stuck, someone has usually hit the same problem and written about it. For a new developer, that last part matters more than any feature list.
What is Laravel?
Taylor Otwell released Laravel in 2011 as a free, open-source PHP framework. It follows the Model-View-Controller (MVC) pattern: routes send a request to a controller, the controller talks to models, and a view renders the response.
What you get out of the box:
- Readable syntax: most code reads close to plain English, like
User::where('active', true)->get() - An ecosystem of first-party tools: Forge, Vapor, Nova, and Horizon cover deployment, admin panels and queue monitoring
- Community: thorough official docs, plus lots of tutorials and packages
- Built-in features: authentication, routing, sessions, caching, and more without extra packages
- Eloquent ORM: database tables mapped to PHP classes, with relationships
Laravel's MVC Architecture Flow
Prerequisites and Installation
You'll need these installed first. Version requirements move with each Laravel release (recent versions need PHP 8.2 or newer), so check the docs for the version you're installing:
| Requirement | Minimum Version | Recommended |
|---|---|---|
| PHP | 8.2 | 8.3+ |
| Composer | 2.0 | Latest |
| Node.js | 18.x | 20.x+ |
| Database | MySQL 5.7 / PostgreSQL 10 | MySQL 8.0 / PostgreSQL 15 |
Step-by-Step Installation
Install Composer
Composer is PHP's dependency manager. Download it from getcomposer.org or use your system's package manager.
Create New Laravel Project
Use the Laravel installer or Composer to create a fresh Laravel application.
# Install Laravel installer globally
composer global require laravel/installer
# Create a new Laravel project
laravel new my-awesome-app
# OR use Composer directly
composer create-project laravel/laravel my-awesome-app
# Navigate to project directory
cd my-awesome-app
# Start the development server
php artisan serve
Configure Environment
The installer usually creates .env for you. If it's missing (say, after cloning a repo), copy .env.example to .env and fill in your database credentials.
Generate Application Key
Run php artisan key:generate to set APP_KEY in .env. Skip this on a cloned project and every page throws "No application encryption key has been specified."
Laravel Sail runs the app in Docker with ./vendor/bin/sail up, along with MySQL, Redis and a few other services. It's handy if you don't want PHP and a database installed locally, but on macOS and Windows file access through Docker can feel slower than a native setup.
Understanding Laravel's Directory Structure
A fresh project has a lot of folders, but you'll spend most of your time in app/, routes/, resources/views/ and database/. Here's the overview. (Since Laravel 11, api.php and channels.php only appear after you run php artisan install:api or install:broadcasting.)
Routing Basics
Every request starts at a route. A route maps a URL and HTTP method to a closure or a controller method, and that's where you decide what happens next.
Define routes in routes/web.php:
use Illuminate\Support\Facades\Route;
// Basic GET route
Route::get('/', function () {
return view('welcome');
});
// Route to a controller
Route::get('/users', [UserController::class, 'index']);
// Different HTTP methods
Route::post('/users', [UserController::class, 'store']);
Route::put('/users/{id}', [UserController::class, 'update']);
Route::delete('/users/{id}', [UserController::class, 'destroy']);
Capture dynamic segments from the URL:
// Required parameter
Route::get('/user/{id}', function ($id) {
return "User ID: " . $id;
});
// Optional parameter with default
Route::get('/user/{name?}', function ($name = 'Guest') {
return "Hello, " . $name;
});
// Multiple parameters
Route::get('/posts/{post}/comments/{comment}', function ($postId, $commentId) {
return "Post: $postId, Comment: $commentId";
});
// Regex constraints
Route::get('/user/{id}', function ($id) {
return "User: " . $id;
})->where('id', '[0-9]+');
Name your routes for easy URL generation:
// Define named route
Route::get('/user/profile', [ProfileController::class, 'show'])
->name('profile');
// Generate URL to named route
$url = route('profile');
// Redirect to named route
return redirect()->route('profile');
// In Blade templates
<a href="{{ route('profile') }}">Profile</a>
// With parameters
Route::get('/user/{id}/profile', [ProfileController::class, 'show'])
->name('profile');
$url = route('profile', ['id' => 1]);
Group routes with shared attributes:
// Middleware group
Route::middleware(['auth'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::get('/settings', [SettingsController::class, 'index']);
});
// Prefix group
Route::prefix('admin')->group(function () {
Route::get('/users', [AdminUserController::class, 'index']);
// URL: /admin/users
});
// Combined attributes
Route::prefix('api')
->middleware('api')
->name('api.')
->group(function () {
Route::get('/users', [ApiUserController::class, 'index'])
->name('users.index');
// Name: api.users.index
});
Controllers
Closures in routes/web.php get messy after a handful of routes. Controllers move that logic into classes, usually one per resource, so everything related to users lives in UserController.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* Display a listing of users.
*/
public function index()
{
$users = User::all();
return view('users.index', compact('users'));
}
/**
* Show the form for creating a new user.
*/
public function create()
{
return view('users.create');
}
/**
* Store a newly created user.
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => bcrypt($validated['password']),
]);
return redirect()
->route('users.show', $user)
->with('success', 'User created successfully!');
}
/**
* Display the specified user.
*/
public function show(User $user)
{
return view('users.show', compact('user'));
}
}
php artisan make:controller UserController --resource creates a controller with empty index, create, store, show, edit, update and destroy methods. Pair it with Route::resource('users', UserController::class) and you get all seven routes in one line.
Eloquent ORM: Working with Databases
Writing raw SQL for every insert and update gets old fast. With Eloquent, Laravel's ORM, each table gets a Model class, and you work with rows as PHP objects instead of arrays.
How Eloquent processes database queries
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class User extends Model
{
use HasFactory;
// Mass assignable attributes
protected $fillable = [
'name',
'email',
'password',
];
// Hidden from arrays/JSON
protected $hidden = [
'password',
'remember_token',
];
// Attribute casting
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
// Relationship: User has many posts
public function posts()
{
return $this->hasMany(Post::class);
}
// Relationship: User belongs to many roles
public function roles()
{
return $this->belongsToMany(Role::class);
}
}
Common Eloquent Operations
// Retrieve all users
$users = User::all();
// Find by primary key
$user = User::find(1);
$user = User::findOrFail(1); // Throws 404 if not found
// Query with conditions
$activeUsers = User::where('status', 'active')
->where('age', '>', 18)
->orderBy('created_at', 'desc')
->get();
// First matching record
$admin = User::where('role', 'admin')->first();
// Create new record
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => bcrypt('password'),
]);
// Update existing record
$user->update(['name' => 'Jane Doe']);
// Delete record
$user->delete();
// Soft delete (if enabled)
$user->delete(); // Marks as deleted
User::withTrashed()->get(); // Include soft deleted
// Eager loading relationships
$users = User::with(['posts', 'roles'])->get();
// Pagination
$users = User::paginate(15);
Looping over User::all() and calling $user->posts inside the loop runs one extra query per user. It's invisible with 10 rows in development and painful with 10,000 in production. Use with() as shown above, and consider adding Model::preventLazyLoading(! app()->isProduction()) in AppServiceProvider so Laravel throws an exception when you forget.
Blade Templates
Views live in resources/views and use Blade, Laravel's templating language. It gives you short directives like @if and @foreach, layout inheritance, and automatic escaping. Blade files compile to plain PHP and get cached, so there's no real runtime cost.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'My Laravel App')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<nav>
@include('partials.navigation')
</nav>
<main>
@yield('content')
</main>
<footer>
@include('partials.footer')
</footer>
</body>
</html>
@extends('layouts.app')
@section('title', 'All Users')
@section('content')
<h1>Users</h1>
{{-- Display success message --}}
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
{{-- Loop through users --}}
@forelse($users as $user)
<div class="user-card">
<h2>{{ $user->name }}</h2>
<p>{{ $user->email }}</p>
@if($user->isAdmin())
<span class="badge">Admin</span>
@endif
<a href="{{ route('users.show', $user) }}">
View Profile
</a>
</div>
@empty
<p>No users found.</p>
@endforelse
{{-- Pagination links --}}
{{ $users->links() }}
@endsection
Blade Directives Quick Reference
| Directive | Purpose | Example |
|---|---|---|
{{ $var }} |
Echo escaped content | {{ $user->name }} |
{!! $var !!} |
Echo unescaped HTML | {!! $post->content !!} |
@if / @else |
Conditional statements | @if($user) ... @endif |
@foreach |
Loop through arrays | @foreach($items as $item) |
@forelse |
Loop with empty fallback | @forelse ... @empty ... @endforelse |
@auth / @guest |
Auth conditionals | @auth Welcome! @endauth |
@csrf |
CSRF token field | <form>@csrf</form> |
@method |
Form method spoofing | @method('PUT') |
Form Validation
Laravel has a few ways to validate input. The simplest is calling validate on the request. Once a controller gets crowded, I'd move the rules into a Form Request class (php artisan make:request StorePostRequest).
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'body' => 'required|min:10',
'category_id' => 'required|exists:categories,id',
'tags' => 'array',
'tags.*' => 'exists:tags,id',
'publish_date' => 'nullable|date|after:today',
'image' => 'nullable|image|mimes:jpg,png|max:2048',
]);
// If validation fails, Laravel automatically redirects back
// with errors. If it passes, continue with validated data.
Post::create($validated);
return redirect()->route('posts.index')
->with('success', 'Post created successfully!');
}
Validate everything that comes from the client, including hidden fields and IDs. Pass $validated to create(), not $request->all(), so users can't slip in fields like is_admin. Validation doesn't escape output for you, though. That's Blade's {{ }} job.
Authentication
Don't write login from scratch. Breeze and Jetstream generate the controllers, views and routes for you, and they handle the details people get wrong, like hashing, throttling and session regeneration. (Laravel 12 also ships newer starter kits through laravel new. Breeze still works and is easy to read.)
# Install Laravel Breeze
composer require laravel/breeze --dev
# Install Breeze scaffolding
php artisan breeze:install
# Choose your stack:
# - blade (Traditional server-side)
# - react (React with Inertia)
# - vue (Vue with Inertia)
# - api (API only)
# Install dependencies and build assets
npm install && npm run dev
# Run migrations
php artisan migrate
You end up with registration, login, password reset, email verification and password confirmation. It's all plain code in your project, so you can change any of it.
Artisan Commands You'll Use Daily
You'll run php artisan constantly, Laravel's CLI. These are the commands I reach for most:
| Command | Description |
|---|---|
php artisan serve |
Start development server |
php artisan make:model Post -mcr |
Create model with migration, controller, and resource |
php artisan migrate |
Run database migrations |
php artisan migrate:fresh --seed |
Fresh migration with seeders |
php artisan db:seed |
Run database seeders |
php artisan tinker |
Interactive REPL for Laravel |
php artisan route:list |
List all registered routes |
php artisan cache:clear |
Clear application cache |
php artisan config:clear |
Clear configuration cache |
php artisan optimize |
Cache config, routes, and views |
Mistakes I See on the First Deploy
Most beginner Laravel apps work fine locally and break on the server for the same few reasons:
Before you go to production
- Wrong document root: point the web server at
public/, not the project root. Otherwise your.envfile, with database passwords, may be downloadable. - Storage permissions: if you see "The stream or file storage/logs/laravel.log could not be opened in append mode: Failed to open stream: Permission denied", the web server user can't write to
storage/andbootstrap/cache/. Fix ownership rather than runningchmod 777. - Debug mode left on: set
APP_DEBUG=falseandAPP_ENV=production. The debug error page shows stack traces and environment details to anyone. - env() outside config files: after
php artisan optimizeorconfig:cache,.envisn't loaded anymore, soenv('SOMETHING')in a controller returnsnull. Read values throughconfig()instead. - Long work in the request: sending emails or resizing images inline makes pages slow. That's the point where queues are worth learning.
When wouldn't I pick Laravel? For a tiny API with two or three endpoints, a microframework or plain PHP is lighter. If your team already knows Node or Django well, switching just for Laravel rarely pays off. And if most of your app is a heavy single-page frontend, Laravel still works as the API, but you won't use Blade much. For a typical CRUD app with users, forms and a database, though, it saves a lot of work. Middleware, Sanctum for APIs, queues and feature tests are the next things worth learning.
Official Documentation: laravel.com/docs
Laracasts: laracasts.com (video tutorials)
Laravel News: laravel-news.com
Laravel Daily: laraveldaily.com
Start small: one model, one resource controller, a couple of Blade views. Once that clicks, the rest of the framework is mostly more of the same pattern.