In the last post I shared how my code has evolved to write clean, succinct, and reusable code. If you haven’t read it, you can check this out below.
https://raheelshan.com/posts/laravel-crud-like-a-pro-clean-reusable-and-ultra-minimal-code
In this post I will go a step further to tell how my previous structure can be even a little more concise. If you are ready, here we go.
From Resource Controller to Custom Controller
We will start by writing a controller again, but this time a custom controller with only 4 to 5 methods that will do all the work for us. Here is the sample.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\Post\GetAllPostsRequest;
use App\Http\Requests\Post\GetPostRequest;
use App\Http\Requests\Post\SavePostRequest;
use App\Http\Requests\Post\DeletePostRequest;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(GetAllPostsRequest $request)
{
$response = $request->handle();
return view('post.index', ['posts' => $response]);
}
/**
* Show the form for creating a new resource.
*/
public function form(GetPostRequest $request,$id)
{
$response = $request->handle($id);
return view('post.form', ['post' => $response]);
}
/**
* Update the specified resource in storage.
*/
public function saveForm(SavePostRequest $request, int $id)
{
$request->handle($id);
if($id > 0){
session()->flash('message', 'Post has been updated successfully.');
}else{
session()->flash('message', 'Post has been created successfully.');
}
return redirect()->route('post.index');
}
/**
* Display the specified resource.
*/
public function show(int $id, GetPostRequest $request)
{
$response = $request->handle($id);
return view('post.show', ['post' => $response]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(int $id, DeletePostRequest $request)
{
$request->handle($id);
session()->flash('message', 'Post has been deleted successfully.');
return redirect()->route('post.index');
}
}
Now let’s define routes for them.
Route::prefix('posts')->name('posts.')->group(function () {
Route::get('/', [PostController::class, 'index'])->name('index');
Route::get('/form/{id?}', [PostController::class, 'form'])->name('form');
Route::post('/form/{id?}', [PostController::class, 'saveForm'])->name('save');
Route::delete('/{id}', [PostController::class, 'delete'])->name('delete');
});
Here we have combined create and edit routes to the get-type form route. Similarly, we have combined store and update routes to post-type form routes.Now let’s see GetPostRequest.
class GetPostRequest extends FormRequest
{
public function handle($id)
{
return Post::findOrNew($id);
}
}
It is as intact as it was last time. We have made no changes in it.Before proceeding, I would like to shed some light on two options available here.
Optional Parameter
You can define a route with an optional parameter and set a default parameter in GetPostRequest.
// optional id parameter. Useful for edit route
Route::get('/form/{id?}', [PostController::class, 'form'])->name('form');
class GetPostRequest extends FormRequest
{
public function handle($id)
{
return Post::findOrNew($id);
}
}
And routes for this
<!-- load form in create mode -->
<a href="{{ route('posts.form') }}">Create</a>
<!-- load form in edit mode inside index.blade.php under $posts loop -->
<a href="{{ route('posts.form', [ 'id' => $post->id ]) }}">
Edit
</a>
Required Parameters
You can define a route with a required parameter and pass 0 in create mode.
// optional id parameter. Useful for edit route
Route::get('/form/{id}', [PostController::class, 'form'])->name('form');
And routes for this.
<!-- load form in create mode by passing 0 -->
<a href="{{ route('posts.form', [ 'id' => 0 ]) }}">Create</a>
<!-- load form in edit mode inside index.blade.php under $posts loop -->
<a href="{{ route('posts.form', [ 'id' => $post->id ]) }}">
Edit
</a>
Now that we have deleted create.blade.php and renamed edit.blade.php to form.blade.php, we can say,
There’s no ‘create’ in real CRUD — just editing something that doesn’t exist yet.
If we take a look at form.blade.php, we will have this.
<form action="{{ route('posts.form', [ 'id' => $post->id ]) }}" method="post">
@csrf
<div>
<div>
<label>Title</label>
<input type="text" name="title" value="{{ old('title', $post->title) }}" />
@error('title')
<div class="text-red-500 text-sm">{{ $message }}</div>
@enderror
</div>
<div>
<label>Content </label>
<textarea name="content"rows="100" cols="80">
{{ old('content', $post->content) }}
</textarea>
@error('content')
<div class="text-red-500 text-sm">{{ $message }}</div>
@enderror
</div>
<div>
<button type="submit">Save</button>
</div>
</div>
</form>
This will suffice to create and edit cases. So combining create and edit forms is worth it. Do pay attention to the second or default parameter in old()\
that will show either the default value in the add case or the populated value in the edit case or the failed-validation-value if form validation fails.
Blade views are now down to two or three.
- index.blade.php to list all posts
- form.blade.php to load new or existing form
- optional show.blade.php to show single post view
Next, we will take a look at SaveFormRequest.
class SavePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title:required|string',
'content:required|string',
];
}
public function handle($id)
{
$data = $this->validated();
if($id > 0){
Post::where('id', $id)->update($data);
}else{
Post::create($data);
}
return true;
}
}
This will add or update a post based on $id.
Planning for Scalability
While centralizing persistence logic inside FormRequests works great for small to medium-sized apps, if the complexity grows you might consider moving heavy operations to service classes or adopting patterns like AQC (Atomic Query Construction).
Summary
- No more resource controller
- Only 4 methods in each controller
- Only 2 blade views for each resource
- Form always loads resource or shows defaults
Final Thoughts
Laravel gives a lot out of the box. If utilized cleverly, you can minimize code as much as possible while still achieving a clean structure with full functionality. I hope you enjoyed this structure — and more importantly, I hope it makes your development smoother and your codebase cleaner.
What’s Next
In the next post, I’ll take this structure even further — making it a little more useful, a little more flexible, without adding any extra noise. Stay tuned. If you want to get notified, do subscribe to my profile.
If you found this post helpful, consider supporting my work — it means a lot.
Top comments (3)
Please watch freely available course Laravel From Scratch. Your Structure, best practices, approaches, everything is mixed up.
Which design pattern are you trying to follow? Maybe it could be you know better approaches than the ones who are the creators of Laravel?
Thanks for your feedback. I’m really just sharing what I’ve personally found useful for reducing boilerplate, keeping controllers lean, and simplifying form logic. It’s not the only way, but simply a different way things could be done. Hopefully my experience still sparks some useful ideas for other developers.
@ali_raza_ here is where i am moving towards. Take a look at 3 part series and i would be happy to get critics.
dev.to/raheelshan/introducing-the-...