DEV Community

Cover image for 🚀 Level Up Your Laravel Requests with DTOs (Data Transfer Objects)
Naveen Kola
Naveen Kola

Posted on • Edited on

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

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'),
        );
    }
}

Enter fullscreen mode Exit fullscreen mode

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,
    ) {}
}

Enter fullscreen mode Exit fullscreen mode

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,
        ]);
    }
}

Enter fullscreen mode Exit fullscreen mode

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),
        ]);
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
xwero profile image
david duymelinck

There is a difference between safe and input. While safe is taking the value from the validator, input is taking the value from the request. So if the validator has and after or passedValidation method the output can be not what you expect it to be.

If you really want to keep using the validated or safe method you could overwrite them.

public function safe(?array $keys = null)
{
    $array = parent::safe($keys);

    // return DTO here
}
Enter fullscreen mode Exit fullscreen mode

I would do that only in case you want all the FormRequest classes to return DTOs. Otherwise the dto method is the way to go.

styling side note: add php after the opening ticks of the code blocks, this adds syntax highlighting.

Collapse
 
naveen_dev profile image
Naveen Kola

Thank you for the valuable feedback 🙏. You make an excellent point about the distinction between safe() (validator output) and input() (raw request data), especially in cases where after() or passedValidation() modifies the values.

Overriding safe() to always return a DTO is an elegant approach for projects where consistency across all FormRequests is required. For now, I’ll continue with the dto() method to maintain flexibility, but I’ll definitely consider adding this alternative as an advanced option in the article.

Also, thank you for the styling tip. I’ll update my code blocks with php for syntax highlighting 👍.