Home / News / 🚀 Level Up Your Laravel Requests with DTOs (Data Transfer Objects)

🚀 Level Up Your Laravel Requests with DTOs (Data Transfer Objects)

When building Laravel applications, we often rely on Form Requests for validation, but it’s crucial to understand the downsides. Here’s a sarcastic take on the topic with ironic hashtags to highlight the flaws in using raw arrays and the request object directly in Laravel Form Requests:

1. #FormRequestProblem 🙄
2. #LaravelValidationMistake 🙈
3. #DTOChallenge 🤦‍♀️
4. #CleanCodeChallenge 🎨
5. #Object-orientedDesignChallenge 🚀
6. #DestructuringChallenge 🤪
7. #SeparationOf ConcernsChallenge 🌖
8. #DumbValidationProblem 😇
9. #SerializationProblem 🤬
When building Laravel applications, we often rely on Form Requests for validation. They’re great for keeping controllers clean and ensuring input data is valid before reaching business logic.

But here’s the problem:
After validation, many developers still use raw arrays or the request object itself to pass data deeper into their application services, repositories, or jobs. This can lead to messy, fragile code that’s hard to maintain.

This is where DTOs (Data Transfer Objects) come in. A DTO acts as a simple, structured object to carry data safely and clearly through your application.

Let’s see how we can use DTOs inside Laravel Form Requests.

🛠 User Registration Flow

Imagine you’re building a user registration feature. Your form collects these inputs:

  • name
  • email
  • password

Step 1: Create a Form Request

<?php

declare(strict_types=1);

namespace App\Http\Requests\Auth;

use App\Http\Requests\Auth\Data\RegisterUserData;
use Illuminate\Foundation\Http\FormRequest;

class RegisterUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users,email'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }

    public function dto(): RegisterUserData
    {
        return new RegisterUserData(
            $this->input('name'),
            $this->input('email'),
            $this->input('password'),
        );
    }
}

Here, we’ve added a dto() method to return a structured object instead of raw inputs.

Step 2: Define the DTO

<?php

declare(strict_types=1);

namespace App\Http\Requests\Auth\Data;

class RegisterUserData
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly string $password,
    ) {}
}

This class represents the validated, immutable registration data.

Step 3: Use the DTO in Your Controller

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\RegisterUserRequest;
use App\Services\Auth\RegisterUserService;

class RegisterController extends Controller
{
    public function __construct(
        private RegisterUserService $registerUserService
    ) {}

    public function store(RegisterUserRequest $request)
    {
        $userData = $request->dto();

        $user = $this->registerUserService->register($userData);

        return response()->json([
            'message' => 'User registered successfully!',
            'user' => $user,
        ]);
    }
}

Now your controller isn’t dealing with arrays or $request->input(). Instead, it passes a clean RegisterUserData DTO to the service.

Step 4: Service Layer Uses the DTO

<?php

declare(strict_types=1);

namespace App\Services\Auth;

use App\Http\Requests\Auth\Data\RegisterUserData;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class RegisterUserService
{
    public function register(RegisterUserData $data): User
    {
        return User::create([
            'name' => $data->name,
            'email' => $data->email,
            'password' => Hash::make($data->password),
        ]);
    }
}

The service doesn’t care about request objects. It only knows it needs a RegisterUserData object. This makes it reusable in CLI commands, jobs, or tests.

💡 Another Example: Payment Checkout

Consider a checkout request with fields like:

  • amount
  • currency
  • payment_method

Instead of juggling arrays like $request->only(['amount', 'currency', 'payment_method']), you can use a DTO:

$checkoutData = $request->dto();

$this->paymentService->process($checkoutData);

This makes your code much clearer and type-safe.

Benefits of Using DTOs in Form Requests

  • Clarity: $request->dto() is cleaner than $request->only(['field1', 'field2']).
  • Type Safety: IDE autocompletion + compile-time checks help prevent mistakes.
  • Immutability: DTOs don’t change once created.
  • Separation of Concerns: Controllers handle requests, services handle logic, DTOs handle data.

🚀 Conclusion

DTOs may seem like a small addition, but they can greatly improve the maintainability and readability of your Laravel projects.

Next time you create a Form Request, don’t just validate inputs, wrap them into a DTO. Your future self (and your teammates) will thank you!

Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *