DEV Community

Cover image for Laravel CRUD Like a Pro: Clean, Reusable, and Ultra-Minimal Code
Raheel Shan
Raheel Shan

Posted on • Originally published at raheelshan.com

Laravel CRUD Like a Pro: Clean, Reusable, and Ultra-Minimal Code

It has been more than 8 years now since I have been using Laravel. Today, I will talk about how my coding practices have evolved from complex to simple — from unnecessarily complex to clean and concise — especially when it comes to CRUD operations.

Laravel CRUD operations are the foundation of the application, and Laravel has introduced many simple methods to do the job well. Here is my implementation.

Generating the ResourceController

We will start with Resource Controller. Let's generate one.

php artisan make:controller PostController --resource
Enter fullscreen mode Exit fullscreen mode

This will generate PostController with default methods.

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        //
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(string $id)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(string $id)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, string $id)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(string $id)
    {
        //
    }
}
Enter fullscreen mode Exit fullscreen mode

Defining the Routes

Next we will define routes for this controller.

use App\Http\Controllers\PostController;

Route::resource('posts', PostController::class);
Enter fullscreen mode Exit fullscreen mode

Creating Form Request Classes

Finally, we will execute some commands to generate FormRequest classes.

php artisan make:request Post\GetAllPostsRequest
php artisan make:request Post\GetPostRequest
php artisan make:request Post\CreatePostRequest
php artisan make:request Post\UpdatePostRequest
php artisan make:request Post\DeletePostRequest
Enter fullscreen mode Exit fullscreen mode

That completes our code generation step. Next we will modify our PostController to have these FormRequest classes and call its handle method, which we will define later.

The Updated Controller

use App\Http\Requests\Post\GetAllPostsRequest;
use App\Http\Requests\Post\GetPostRequest;
use App\Http\Requests\Post\CreatePostRequest;
use App\Http\Requests\Post\UpdatePostRequest;
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 create(GetPostRequest $request)
    {
        $response = $request->handle();
        return view('post.create', ['post' => $response]);
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(CreatePostRequest $request)
    {
        $request->handle();
        session()->flash('message', 'Post has been saved 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]);
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(int $id, GetPostRequest $request)
    {
        $response = $request->handle($id);
        return view('post.edit', ['post' => $response]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdatePostRequest $request, int $id)
    {
        $request->handle();
        session()->flash('message', 'Post has been updated successfully.');
        return redirect()->route('post.index');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(int $id, DeletePostRequest $request)
    {
        $request->handle();
        session()->flash('message', 'Post has been deleted successfully.');
        return redirect()->route('post.index');
    }
}
Enter fullscreen mode Exit fullscreen mode

You can see how simple and lean our controller is.

  • We have set flash messages when our operation is done.
  • We have defined redirects when our operation is successful.
  • Every method of our controller has identical implementation.

Diving into Form Request Classes

Now let's move to our FormRequest classes. As you can see, I have defined the handle method in each class we will talk about here. Let's start with GetAllPostsRequest.

class GetAllPostsRequest extends FormRequest
{
    public function handle()
    {
        $params = $this->all();

        $postObj = Post::latest('id');

        if (isset($params['status'])) {
            $postObj->where('status', $params['status']);
        }

        return $postObj->paginate(Post::PAGINATE);
    }
}
Enter fullscreen mode Exit fullscreen mode

The handle method is pretty simple. It defines pagination logic as well as filtering records if required.

Tip: You can move the filtering logic to scopes.

Next, let’s see GetPostRequest.

class GetPostRequest extends FormRequest
{
    public function handle($id)
    {
        return Post::findOrNew($id);
    }
}
Enter fullscreen mode Exit fullscreen mode

GetPostRequest is even simpler. It defines a very simple method: just grab a record by ID. Since it is called in 3 methods in the controller—create, edit, and show—consider the following:

  • For create method where id is 0, it will fetch a new record with empty or default values
  • For edit and show methods it will query a record based on ID.
  • By using the findOrNew() method, our logic is simplified and reusable.

In the blade template create.blade.php or edit.blade.php, you do this.

<input type="text" name="title" value="{{ old('title', $post->title) }}" />
Enter fullscreen mode Exit fullscreen mode

This not only shows the previously entered input if validation failed but also populates data from the database if we are in edit mode. In the add/create mode, since we are using the findOrNew() method, it will give us a post title with an empty string.

At this point our record-fetching logic is complete. Let’s move to record-saving logic.

class CreatePostRequest 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()
    {
        $data = $this->validated();

        Post::create($data);

        return true;
    }   
}
Enter fullscreen mode Exit fullscreen mode

We call the validated() method before proceeding. This will give us the following benefits:

  • If validation logic fails it will redirect user to form with validation errors
  • If validation passes, the record will be saved

Tip: Use $fillable along with the create() method.

The UpdatePostRequest has identical implementation to the CreatePostRequest.

class UpdatePostRequest extends FormRequest
{
    /**
     * 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();

        Post::where('id', $id)->update($data);

        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

And here is the DeletePostRequest.

class DeletePostRequest extends FormRequest
{
    public function handle($id)
    {
        Post::where(['id' => $id])->delete();
        return true;
    }    
}
Enter fullscreen mode Exit fullscreen mode

Why use FormRequests?

Our CRUD implementation logic is complete now. You might be wondering why we have chosen to use FormRequests instead of defining our logic directly in the controller. Consider the following benefits:

Authorization

FormRequest gives us a simple authorize method; if used effectively, you can control action authorization. Let's say you are using Bouncer. In GetPostRequest, define this to control access.

public function authorize(): bool
{
    return Bouncer::can('view-post');
}
Enter fullscreen mode Exit fullscreen mode

Handle Method for Business Logic

This is rather a custom implementation to define business logic at some relevant place. For further insights, read this post.

Lean Controller

You can see our controller has as little code as possible, with all the methods having identical structure that is easy to understand and remember.

Tip: You can replace the logic of returning model data in HTML or AJAX format. For example, in the index method, use this.

return ResponseHelper::handle('index', $data);
Enter fullscreen mode Exit fullscreen mode

The ResponseHelper Class

class ResponseHelper
{
    public static function handle($view, $data)
    {
        if (request()->is('api/*')) {
            return response()->json($data);
        }

        return view($view,$data);
    }
}
Enter fullscreen mode Exit fullscreen mode

This will return the response in AJAX format if called from an API or in HTML format if called from the web.

Identical Pattern

Since all our ForRequest classes follow the same pattern, if we strictly stick to this, our application will have clean and concise code no matter how big the application grows.

One Liner Code

This one is the most satisfying point. The actual reason why I have written this post. Have you noticed? If not, let me repeat by indicating some code.

$postObj = Post::latest('id');

if (isset($params['status'])) {
    $postObj->where('status', $params['status']);
}

return $postObj->paginate(Post::PAGINATE);

/* --------------------------- */

return Post::findOrNew($id);

/* --------------------------- */

Post::create($data);

/* --------------------------- */

Post::where('id', $id)->update($data);

/* --------------------------- */

Post::where(['id' => $id])->delete();
Enter fullscreen mode Exit fullscreen mode

This is the power of Laravel that gives us control to write as little code as possible but do as much work as we want.

Final Thoughts

Laravel gives us elegant tools to simplify CRUD operations. By moving logic to FormRequest classes and keeping controllers clean, we write less and achieve more.

This post summarizes how my coding practices have evolved from complex to simple. I will write yet another post on this topic, cutting down some unnecessary parts from this structure. If you are interested, you can subscribe to my profile.

Top comments (6)

Collapse
 
xwero profile image
david duymelinck

You really should stop using the FormRequest class as a jack of all trades object.

In the controller you now have handle functions that could mean anything. So other developers have to go to that method to see what it does.

With the one liner code section you contradicting the lean controller argument.
If you wanted to abstract that list of one liners it should be in a repository class.

You don't need a FormRequest to authorise requests. The most versatile method is to use middleware.

class CheckAbility
{
    public function handle(Request $request, Closure $next, iterable|string $ability)
    {
        if (!Bouncer::can($ability)) {
            abort(403, 'Unauthorized.');
        }

        return $next($request);
    }
}
// in routes/web.php
Route::get('/post/{id}')->middleware(CheckAbility::class . ':view-post');
Enter fullscreen mode Exit fullscreen mode

If you only use the Bouncer can method, just use Laravels' Gate allows method.

You didn't write less code, you wrote more code. And the biggest flaw with your simplification is that you lost context. The reason why there are separate classes for different tasks is to give the methods more context.
If a see a FormRequest method with the name handle I assume it handles the form data from the request. I don't know if you are familiar with smurfs? But they have their own language where most words are replaced with the word smurf. And then you get situations like this comic book page.

Collapse
 
raheelshan profile image
Raheel Shan • Edited

Hi @xwero. Thanks for your feedback.

I see your point about turning FormRequest into a “jack of all trades.” But my main motivation is to reduce repetitive boilerplate and keep the request as informative as possible. This reduces context-switching between multiple classes. Instead, I can open a single request class and see the full lifecycle of that request.

Regarding handle(), I actually find that abstraction useful. It might look like smurfing, but the convention is consistent. Every request class has a handle() method that executes the main action. When I see SomeRequest->handle(), I know exactly where to look.

Middleware is definitely a good approach, and I agree it’s versatile. But form request authorize is definitely useful when used in role-based access system. That way, the request class becomes self-contained: it knows who can perform the action and what data is valid.

About writing “more code”: I see it differently. Even if the raw line count looks higher, the benefit comes from removing duplication and having a uniform structure. For me, “less code” means less mental overhead when maintaining the app—not just fewer lines in a file.

In the end, it’s a matter of trade-offs. Your approach is great for strict separation and clarity. My approach is optimized for rapid development and minimal boilerplate, especially in teams or projects where consistency is more valuable than rigid separation.

I wouldn’t use this pattern in every project, but for the scenarios I wrote about in the article, it has proven to be a huge productivity booster.

Finally i would say this post is for beginner-level developers. Thats how my patterns have evolved. This is only first step. Later you will see how i turn them to service based patterns. For now just see it as a different way to achieve a task.

Collapse
 
xwero profile image
david duymelinck

This reduces context-switching between multiple classes. Instead, I can open a single request class and see the full lifecycle of that request

Following that logic the best place for the code would be in your controller method.

When I see SomeRequest->handle(), I know exactly where to look.

The problem with that sentence is I. It is a fact that your code will be read by other people.
What is easier for you; let people read the code and understand what it does or let people read the code and ask you what is does.
A convention only works if everyone knows it.

What you are doing is what I consider to be an "enough knowledge to be dangerous" practice.
You read/heard about thin controllers, but you don't want the multiple objects for the separation of concerns.
You seen the handle method used in single method classes, but you ignore that FormRequest is not a single method class.

Finally i would say this post is for beginner-level developers. Thats how my patterns have evolved. This is only first step.

That is why I am reacting. This should never be considered as a good coding practice.
Use the objects as they are intended to be used.
You are not going to use a model to render a view? Why would the FormRequest class be different?

Thread Thread
 
raheelshan profile image
Raheel Shan

Hi @xwero, I appreciate the follow-up.

I get your point about controllers being the natural home for this logic. That’s true in the strict sense of Laravel conventions. My reason for leaning on FormRequest is to bring validation, authorization, and handling closer together so beginners (the main audience of this post) can see the full lifecycle in one place. It’s not about ignoring conventions, but about reducing the “jumping around” problem that often confuses people early on.

On the handle() method: you’re right that a convention only works if people know it. In my projects, it’s a simple, predictable pattern that helps keep things uniform. For someone outside that convention, yes, it may require a quick explanation—but the same is true for any custom abstraction or pattern until it’s learned.

I also want to stress that I don’t consider this the final pattern or something to blindly use in all projects. As I mentioned, this is step one, helping developers move away from fat controllers. Over time, this naturally evolves into service classes or repositories depending on the project size and team. My goal here was to give beginners a stepping stone, not to replace Laravel’s conventions.

So I agree with you that this isn’t the one right way or necessarily the most scalable approach. But as an educational starting point, it has proven to be useful, and from there I show how it matures into more robust, conventional patterns.

Thread Thread
 
xwero profile image
david duymelinck

I understand what you are trying to do. The problem is you are learning people to create the wrong abstractions.

Fat controllers are not the main problem of unmaintainable code. The tight coupling of the abstractions is the main problem.

Because the FormRequest class has a dependency on the Validator class, it makes no sense that you use it for the index and show methods.

If beginners aren't able/willing to learn to divide crucial parts like validation (FormRequest), authorization (middleware) and data retrieval and manipulation (model/repository) they should make an effort.

Thread Thread
 
raheelshan profile image
Raheel Shan

I think I have made my points clear why I do this. Let's leave it to people to adopt or reject this approach.