Laravel / Web Development
Laravel Features I Ignored for Way Too Long
I used to write custom solutions for problems Laravel had already solved. From validation and authorization to queues, scheduled tasks, and database performance, these eight features changed how I approach building applications. Sometimes, writing better Laravel code starts with writing less of it.
Eight built-in Laravel features that reduce boilerplate, simplify application architecture, improve performance, and make everyday development more productive.
Back to blogLaravel Features I Ignored for Way Too Long
Laravel makes it surprisingly easy to build applications.
You can create routes, write controllers, interact with a database, and ship working features without understanding everything the framework provides.
That accessibility is one of Laravel's greatest strengths.
But it also creates an interesting problem: you can become productive enough to build applications while still writing code that Laravel could have handled for you.
I used to approach features by asking how I could implement them myself.
How should I validate this request? Where should I check permissions? How should I format this API response? How do I run this task automatically?
The answers often involved another helper method, another conditional, another custom service, or another piece of repetitive code.
Eventually, I started asking a different question:
Does Laravel already solve this?
Here are eight Laravel features I wish I had paid more attention to earlier, and why they deserve a place in your everyday development workflow.
1. Form Requests: Stop Putting All Your Validation Inside Controllers
When you're starting with Laravel, validating directly inside a controller feels natural.
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
]);
return Post::create($validated);
}
There is nothing inherently wrong with this approach.
For a small endpoint, it can be perfectly reasonable.
The problem appears when validation becomes more complicated.
You start introducing conditional rules, custom messages, input normalization, and additional authorization requirements.
Your controller becomes responsible for too many things.
A better approach: Form Requests
Laravel provides dedicated Form Request classes for handling validation and request-level authorization.
Generate one using Artisan:
php artisan make:request StorePostRequest
Define your validation rules:
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => [
'required',
'string',
'max:255',
],
'content' => [
'required',
'string',
],
];
}
}
Your controller becomes much simpler:
public function store(StorePostRequest $request)
{
$post = Post::create(
$request->validated()
);
return to_route('posts.show', $post);
}
Laravel automatically validates the request before the controller action executes.
If validation fails, Laravel produces the appropriate validation response.
You can also use prepareForValidation() to normalize incoming data before applying validation rules.
When I use it
I prefer Form Requests for application write operations, especially when validation rules are reused, become conditional, or need dedicated tests.
For complex workflows, I pass the validated values into a focused Action rather than passing the entire HTTP request.
The goal is simple: the controller should coordinate the request, not become the home for every responsibility.
2. Policies: Stop Scattering Authorization Checks Everywhere
Consider a blog application where users can edit their own posts.
One approach is to write authorization checks directly inside controllers:
if ($post->user_id !== auth()->id()) {
abort(403);
}
This works until you need the same rule in multiple places.
You might need to check permissions when editing, deleting, publishing, or viewing a post.
Before long, authorization rules are scattered across controllers and duplicated throughout the application.
That makes them harder to maintain and easier to apply inconsistently.
A better approach: Policies
Policies organize authorization rules around a model or resource.
Generate one:
php artisan make:policy PostPolicy --model=Post
Define the authorization rule:
class PostPolicy
{
public function update(
User $user,
Post $post
): bool {
return $user->id === $post->user_id;
}
}
Then authorize the operation in your controller:
public function update(
UpdatePostRequest $request,
Post $post
) {
$this->authorize('update', $post);
$post->update($request->validated());
return to_route('posts.show', $post);
}
The policy becomes the authoritative place for that authorization decision.
If your ownership rules change later, you have a clear location to update them.
When I use it
Policies are especially valuable in applications with multiple user roles, ownership rules, administrative functions, and sensitive data.
For example, a role-management application might need separate rules for creating roles, assigning permissions, and deleting protected system roles.
Those checks belong in the backend, regardless of whether the frontend hides certain buttons.
Hiding an action in React is a user-interface decision. Enforcing authorization in Laravel is a security requirement.
Form Requests and Policies can also work together. A Form Request may call a policy in its authorize() method, or the controller can perform authorization explicitly.
Whichever approach you choose, keep authorization consistent and test both permitted and denied operations.
3. API Resources: Stop Manually Building Every JSON Response
When building APIs, it is tempting to return Eloquent models directly.
return response()->json($user);
This is convenient, but the shape of your API response can become dependent on your model's attributes, hidden fields, and loaded relationships.
Another common approach is to manually construct response arrays in controllers.
return response()->json([
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
]);
This works, but repeatedly defining similar response structures becomes difficult to maintain.
A better approach: API Resources
Laravel's API Resources provide a dedicated transformation layer between Eloquent models and the responses sent to clients.
Generate a resource:
php artisan make:resource UserResource
Define the response structure:
class UserResource extends JsonResource
{
public function toArray(
Request $request
): array {
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at
->toIso8601String(),
];
}
}
Then return the resource:
public function show(User $user): UserResource
{
return new UserResource($user);
}
For collections, Laravel also supports:
return UserResource::collection(
User::query()->paginate(15)
);
Resources provide a consistent place to define which information your API exposes and how that information is represented.
They also support conditional attributes and relationships.
For example:
'posts' => PostResource::collection(
$this->whenLoaded('posts')
),
This includes the relationship when it has already been loaded.
The resource does not need to fetch the relationship itself.
When I use it
I use API Resources when building REST APIs, mobile application backends, and frontend integrations that need predictable response structures.
They are particularly helpful when multiple clients consume the same API.
Your database schema and your public API contract are different responsibilities. API Resources help keep them separate.
For applications using Inertia, Resources can also be useful when a page needs a deliberate, reusable data representation rather than a raw model.
4. Model Casts: Let Eloquent Handle Data Conversion
Imagine working with a model containing boolean flags, JSON settings, dates, and monetary values.
Without understanding Eloquent casts, you may find yourself repeatedly converting those values throughout the application.
$settings = json_decode(
$user->settings,
true
);
Or:
$isActive = (bool) $user->is_active;
Or manually converting date strings every time you need to perform date calculations.
Laravel already provides a mechanism for defining how model attributes should be converted.
A better approach: Model Casts
Define the casts directly on your model:
class User extends Model
{
protected function casts(): array
{
return [
'settings' => 'array',
'is_active' => 'boolean',
'email_verified_at' => 'datetime',
];
}
}
Now you can work with those attributes using their expected PHP types:
$user->settings['theme'];
$user->is_active;
$user->email_verified_at?->format('Y-m-d');
You can also cast attributes to PHP enums.
protected function casts(): array
{
return [
'status' => OrderStatus::class,
];
}
Then work with the enum directly:
if ($order->status === OrderStatus::Paid) {
// Handle the paid order.
}
This avoids scattering raw status strings throughout your application.
When I use it
I use model casts for dates, booleans, JSON attributes, enums, and value-like data types.
For more complicated transformations, Laravel supports custom casts.
One important consideration is that casts do not replace database constraints or input validation. They are a model-level representation mechanism.
For money, I also avoid assuming that a floating-point cast provides exact financial arithmetic. Integer minor units or appropriate decimal-handling strategies are better suited to values that require precise calculations.
Let the model define how its attributes should behave instead of repeatedly converting them throughout the application.
5. Queues: Stop Making Users Wait for Work They Don't Need to Wait For
Consider a user submitting a form.
The application saves the record, sends an email, generates a report, and calls an external API.
If all of those operations happen synchronously, the user waits for every operation to finish before receiving a response.
That is unnecessary when some of the work can happen independently.
A better approach: Queues
Laravel's queue system allows you to move suitable tasks out of the request-response cycle.
Generate a job:
php artisan make:job GenerateReport
Define the work:
class GenerateReport implements ShouldQueue
{
use Queueable;
public function __construct(
public int $reportId
) {}
public function handle(): void
{
$report = Report::findOrFail(
$this->reportId
);
// Generate and store the report.
}
}
Dispatch it from your application:
GenerateReport::dispatch($report->id);
With an asynchronous queue connection and a running worker, the job can execute separately from the HTTP request.
Run a worker:
php artisan queue:work
For development, you can use Laravel's database queue driver without introducing a separate message broker.
For production applications, you should also consider worker supervision, retries, failed-job handling, timeouts, and the actual queue infrastructure your deployment supports.
When I use it
Queues are useful for operations such as sending notifications, processing uploads, generating exports, and communicating with external services.
However, not every operation should be queued.
If the user needs the result immediately to complete the request, that work may belong in the synchronous workflow.
Likewise, an essential database operation should not be moved into a queue merely to make the response faster.
If a job depends on records created inside a database transaction, use an appropriate after-commit dispatch strategy so the worker does not attempt to read data before it has been committed.
Queue work that can safely happen later. Keep essential work in the request when the user needs its result immediately.
6. Events and Listeners: Stop Coupling Every Secondary Action Together
Imagine an application that publishes a blog post.
After publishing, it might need to notify subscribers, update a search index, and record an analytics event.
A straightforward implementation might place all of those operations inside the same Action.
$post->publish();
$notificationService->notify($post);
$searchService->index($post);
$analyticsService->record($post);
For a small application, this can be understandable.
But as secondary reactions grow, the publishing workflow becomes tightly coupled to every system that needs to react to it.
A better approach: Events and Listeners
Laravel provides events and listeners for organizing reactions to meaningful application occurrences.
Generate an event:
php artisan make:event PostPublished
Define the event:
class PostPublished
{
use Dispatchable, SerializesModels;
public function __construct(
public Post $post
) {}
}
Dispatch it after the post has been published:
$post->publish();
PostPublished::dispatch($post);
A listener can respond to that event:
class NotifySubscribers
{
public function handle(
PostPublished $event
): void {
// Notify subscribers about the post.
}
}
Laravel can register listeners through event discovery or explicit event configuration.
Listeners can also implement ShouldQueue when their work is suitable for asynchronous execution.
When I use it
Events are useful when several independent parts of an application need to respond to the same occurrence.
However, I avoid using events to hide essential business workflows.
For example, if publishing a post must update several database records atomically, I prefer to keep those required operations explicit in the Action and transaction.
Events are better suited to secondary reactions, such as notifications or analytics.
They also do not automatically guarantee that a listener succeeds. Critical reactions still need appropriate failure handling, transaction coordination, and testing.
Events should make your application's behavior easier to extend, not make its essential workflow harder to trace.
7. Scheduled Commands: Stop Running Recurring Tasks Manually
Some application operations need to happen repeatedly.
You might need to expire unused tokens, clean temporary files, generate daily summaries, or check records that require attention.
A common early approach is to perform these tasks manually or write one-off scripts that eventually become difficult to manage.
Laravel already provides a scheduler for defining recurring application tasks.
A better approach: Laravel's Task Scheduler
You can define schedules directly in your application.
For example, in routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:generate')
->daily()
->withoutOverlapping();
This schedules a custom Artisan command to run once per day.
The withoutOverlapping() method helps prevent another instance from starting while a previous scheduled instance is still running, using Laravel's cache-based locking mechanism.
To create the command:
php artisan make:command GenerateReports
Laravel can also schedule closures and queued jobs when appropriate.
In a typical production setup, a cron entry invokes Laravel's scheduler every minute:
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
The scheduler then determines which tasks are due.
For local development, Laravel also provides:
php artisan schedule:work
When I use it
Scheduled commands are useful for recurring maintenance, data synchronization, reminders, reporting, and periodic cleanup.
One important distinction: defining a schedule does not automatically make it run.
Your deployment must provide the scheduler process or a suitable platform-level scheduling mechanism.
For applications running across multiple servers, consider onOneServer() where appropriate and ensure the servers share a compatible cache backend.
Scheduled tasks should also be designed to handle retries and repeated execution safely.
8. Eager Loading: Stop Accidentally Creating N+1 Queries
This was one of the most important Eloquent lessons for me.
Imagine displaying a list of posts along with their authors.
You retrieve the posts:
$posts = Post::all();
Then display each author:
foreach ($posts as $post) {
echo $post->user->name;
}
At first glance, the code looks completely normal.
However, when the user relationship has not been loaded, accessing it may execute another database query for each post.
If you retrieve 100 posts, you could execute one query for the posts and another 100 queries to retrieve their authors.
That is the classic N+1 query problem.
A better approach: Eager Loading
Tell Eloquent which relationships you need when retrieving the records:
$posts = Post::query()
->with('user')
->get();
Eloquent can then retrieve the posts and their related users in separate, batched queries rather than querying the author once for every post.
For paginated results:
$posts = Post::query()
->with('user')
->latest()
->paginate(15);
You can also eager load nested relationships:
$posts = Post::query()
->with([
'user',
'comments.user',
])
->paginate(15);
And when you only need a relationship count:
$posts = Post::query()
->withCount('comments')
->paginate(15);
This avoids loading every comment model just to determine how many comments a post has.
When I use it
I review eager loading whenever an endpoint returns collections of models with related data.
This is especially important for dashboards, administrative tables, paginated API responses, and reporting interfaces.
Laravel also provides mechanisms to prevent unintended lazy loading during development.
Model::preventLazyLoading(
! app()->isProduction()
);
With this configured in an appropriate service provider, accidental lazy loading can surface earlier during development rather than becoming a production performance issue.
Eager loading is not about loading every possible relationship by default.
It is about loading the relationships the application actually needs.
A small amount of attention to database queries can make a significant difference in application performance.
The Bigger Lesson: Learn What Your Framework Already Provides
These eight features solve different problems, but they share the same underlying lesson.
You do not always need to build another abstraction to make your application more maintainable.
Sometimes you need to understand the abstractions Laravel has already provided.
Form Requests organize validation.
Policies organize authorization.
API Resources define response structures.
Model casts handle attribute representation.
Queues move suitable work outside the request-response cycle.
Events and listeners separate independent reactions.
Scheduled commands handle recurring operations.
Eager loading helps prevent unnecessary database queries.
When these features work together, they create a practical foundation for building applications without introducing unnecessary complexity.
A typical application workflow might look like this:
HTTP Request
↓
Form Request
↓
Controller + Policy
↓
Focused Action
↓
Eloquent Models
↓
Database
↓
API Resource / Response
Secondary work can be handled through events, listeners, and queued jobs.
Recurring operations can be handled through Laravel's scheduler.
Database reads can use eager loading when related data is required.
Each component has a clear responsibility.
You can still introduce additional architectural patterns when the application genuinely needs them. The point is to understand the existing framework before creating a replacement for something it already does.
How I Approach New Laravel Features Now
Before implementing a new feature, I try to ask a few questions.
Is this an HTTP validation concern?
Use a Form Request when dedicated request validation makes the endpoint clearer.
Is this an authorization decision?
Consider a Policy instead of scattering permission checks throughout the application.
Is this a data representation concern?
Consider an API Resource or model cast.
Does this work need to happen before the user receives a response?
If not, determine whether a queued job is appropriate.
Is this a secondary reaction to a meaningful application event?
Consider an event and listener, while keeping essential operations explicit.
Does this task need to run repeatedly?
Consider Laravel's scheduler.
Am I accessing relationships inside a collection?
Review the generated queries and consider eager loading.
These questions do not mean every feature must use every Laravel component.
A simple endpoint does not need multiple layers merely to satisfy an architectural checklist.
The objective is to select the right tool for the actual problem.
Final Thoughts
Laravel is easy to start with because it does not force you to understand every framework feature before building something useful.
You can ship applications with basic routes, controllers, and Eloquent models.
But as your projects grow, learning the framework's existing capabilities becomes increasingly valuable.
The difference is not necessarily writing more sophisticated code.
Sometimes it is recognizing that the code you were about to write is already unnecessary.
Before implementing another custom solution, ask yourself:
"Does Laravel already solve this?"
You might discover that the framework has been waiting for you to use a feature that could have saved you hours of work.
And that is one of the most useful lessons I have learned while building with Laravel.