A Handy Way to Handle API Validation in Laravel
We all want our code to be clean, maintainable, and high-quality. As someone who really values good architecture, I want to share an idea on how to avoid creating tons of repetitive Form Request classes in Laravel - you know, those like CreateUserRequest, UpdateUserRequest, and so on. But before diving into that, let’s start with how to properly handle and display validation error messages for APIs. Validation Messages: The Responsable Contract The Illuminate\Contracts\Support\Responsable contract was introduced in Laravel 5.5 to standardize how HTTP responses are returned from controllers and routes. When an object implements this interface, Laravel automatically calls its toResponse() method whenever you return that object from a controller or route closure. Here’s what the interface looks like: public function toResponse($request); $request is the current HTTP request instance (Illuminate\Http\Request). The return value is a response object - usually something that extends Symfony\Component\HttpFoundation\Response, like a standard Laravel Response, JsonResponse, and so on. Here’s an example of how you might use this for validation error messages:

We all want our code to be clean, maintainable, and high-quality. As someone who really values good architecture, I want to share an idea on how to avoid creating tons of repetitive Form Request classes in Laravel - you know, those like CreateUserRequest, UpdateUserRequest, and so on.
But before diving into that, let’s start with how to properly handle and display validation error messages for APIs.
Validation Messages: The Responsable Contract
The Illuminate\Contracts\Support\Responsable
contract was introduced in Laravel 5.5 to standardize how HTTP responses are returned from controllers and routes. When an object implements this interface, Laravel automatically calls its toResponse()
method whenever you return that object from a controller or route closure.
Here’s what the interface looks like:
public function toResponse($request);
$request
is the current HTTP request instance (Illuminate\Http\Request
). The return value is a response object - usually something that extends Symfony\Component\HttpFoundation\Response
, like a standard Laravel Response
, JsonResponse
, and so on.
Here’s an example of how you might use this for validation error messages:
errors = $errors;
}
/**
* Converts the response to a JSON response.
*
* @param mixed $request
* @return \Illuminate\Http\JsonResponse
*/
public function toResponse($request): JsonResponse
{
$requestId = Context::get(key: 'request_id');
$timestamp = Context::get(key: 'timestamp');
return new JsonResponse(
data: [
'status' => Response::HTTP_UNPROCESSABLE_ENTITY,
'result' => [
'message' => __(key: 'Validation error.'),
'errors' => $this->errors
],
'metadata' => [
'request_id' => $requestId,
'timestamp' => $timestamp,
],
],
status: Response::HTTP_UNPROCESSABLE_ENTITY
);
}
}
Now, we can simply return this object from our Form Requests to handle and display validation error messages.
Universal Base Request Class
So, to start off, let’s centralize and standardize the logic in our base Request class:
|string>
*/
abstract public function rules(): array;
/**
* Handle failed validation.
*
* @param Validator $validator
* @return void
*/
protected function failedValidation(Validator $validator): void
{
$response = new ValidationResponse(errors: $validator->errors());
throw new HttpResponseException(
response: $response->toResponse(request: $this->request)
);
}
}
The key idea here is to avoid duplicating the authorize()
and failedValidation(Validator $validator)
methods in every single Form Request. Instead, we put them into a shared base Request class where we define the default behavior.
When needed, individual child classes can override these methods to customize the logic for specific requests. This approach greatly reduces repetitive code and makes maintenance much easier.
Now, let’s move on to the concrete implementation of our base Request class.
Flexible Form Request Implementation
Instead of creating separate Form Request classes for each method (like create and update), you can use $this->method()
together with the match expression. This lets you compactly define different validation rules within a single class depending on the HTTP method of the request, avoiding code duplication.
Let’s look at an example based on the PublishRequest
class:
>
*/
private const array COMMON_RULES = [
'avatar' => [
'bail',
'nullable',
'image',
'mimes:jpeg,jpg,png,gif',
'max:3072'
],
'first_name' => ['bail', 'required', 'string', 'min:2', 'max:18'],
'last_name' => ['bail', 'nullable', 'string', 'min:2', 'max:27'],
'status' => ['bail', 'nullable', 'boolean'],
'role_id' => ['bail', 'nullable', 'uuid', 'exists:roles,id']
];
/**
* Returns validation rules depending on HTTP method.
*
* @return array>
*/
public function rules(): array
{
$pwdRules = Password::min(size: 8)->letters()->numbers()->symbols();
$rules = match ($this->method()) {
'POST' => [
'email' => $this->emailRules(unique: true),
'password' => [
...['bail', 'required', 'string', 'confirmed'],
...[$pwdRules]
],
],
'PUT', 'PATCH' => [
'id' => ['bail', 'required', 'uuid', 'exists:users,id'],
'email' => $this->emailRules(unique: false),
'password' => [
...['bail', 'sometimes', 'string', 'confirmed'],
...[$pwdRules]
],
],
default => []
};
return [...self::COMMON_RULES, ...$rules];
}
/**
* Generates email validation rules.
*
* @param bool $unique
* @return array
*/
private function emailRules(bool $unique): array
{
$emailRules = [
'bail',
'required',
'email:rfc,strict,spoof,dns',
'max:254'
];
if ($unique) {
$emailRules[] = 'unique:users,email';
} else {
$emailRules[] = 'unique:users,email,' . $this->id;
}
return $emailRules;
}
/**
* Converts 'status' input to boolean or null.
*
* @return void
*/
protected function prepareForValidation(): void
{
if ($this->has(key: 'status')) {
$this->merge(input: ['status' => filter_var(
value: $this->boolean(key: 'status'),
filter: FILTER_VALIDATE_BOOLEAN,
options: FILTER_NULL_ON_FAILURE
)]);
}
}
}
Why is this convenient and effective?
Single class for multiple scenarios - no need to create separate classes for create and update actions, which reduces code duplication and makes maintenance easier.
Reusing rules - common validation rules are stored in the
COMMON_RULES
constant, eliminating repetition.Data preparation before validation - the
prepareForValidation
method ensures the status field is always correctly interpreted as a boolean or null.
Overall, this approach helps you write cleaner, more maintainable, and safer code, especially in projects that involve many different operations on the same entities.