Ir al contenido principal
Laravel, shipping fast.
Capitulo 2 · Arquitectura central

Clases FormRequest: validacion centralizada

Julian Beaujardin

Algo que me encanta de Laravel: la validación no pertenece a tu controlador. Gracias a la inyección de dependencias puedes moverla a una clase FormRequest dedicada y dejar que Laravel la resuelva y la valide automáticamente antes incluso de que corra tu controlador. Es más limpio, más testable, más reutilizable, y mantiene tu controlador centrado en lo que debería hacer. Así que, en lugar de escribir validación en línea, define las reglas en una clase dedicada:

// The base every request in the service extends. It exists for one reason:
// a failed validation must come back shaped like every other error.
abstract class BaseFormRequest extends FormRequest
{
    /**
     * @return array<string, ValidationRule|array<mixed>|string>
     */
    abstract public function rules(): array;

    protected function failedValidation(Validator $validator): never
    {
        throw new HttpResponseException(
            response: (new ErrorResponse(
                message: (string) json_encode($validator->errors()->toArray()),
            ))->toResponse($this)
        );
    }
}

Lo que Laravel hace por defecto ante una validación fallida es redirigir, que es lo correcto para un formulario y lo incorrecto para una API. Sobrescribir failedValidation() una vez significa que un fallo de validación llega con el mismo sobre { "errors": [...] } que cualquier otro fallo. Escríbelo en la base, y ningún request concreto podrá olvidarse.

class CreateLicenseRequest extends BaseFormRequest
{
    /**
     * @return array<string, array<int, string>>
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:100'],
            'domain' => ['required', 'string', 'max:100'],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => Lang::get('validation.name.required'),
            'name.string' => Lang::get('validation.name.string'),
            'name.max' => Lang::get('validation.name.max'),
            'domain.required' => Lang::get('validation.domain.required'),
            'domain.string' => Lang::get('validation.domain.string'),
            'domain.max' => Lang::get('validation.domain.max'),
        ];
    }
}

El request concreto son sólo reglas y mensajes. Ese es justamente el punto: la decisión interesante —qué forma tiene un fallo— se tomó una vez, en otro sitio.

Por qué importa esto: un FormRequest no es sólo un sitio donde poner reglas de validación. Es un sistema que mejora tu API de formas concretas.

Validación centralizada significa que todas tus reglas viven en un sitio. Cuando alguien necesite entender qué datos acepta tu endpoint, lee el FormRequest, en lugar de rastrear un controlador buscando validación en línea. Cuando haya que cambiar las reglas —quizá domain deba aceptar cadenas más largas, o haya un campo nuevo— actualizas un fichero. Todo lo que usa ese FormRequest recibe automáticamente las reglas nuevas. Sin cambios dispersos por varios controladores. Sin la inconsistencia de que un endpoint valide distinto que otro.

La reutilización llega sola una vez separada la validación. El mismo CreateLicenseRequest puede usarse desde varios controladores si varios endpoints necesitan esas mismas validaciones. No copias y pegas lógica de validación. No te preguntas si este controlador tiene las mismas reglas que aquel. Usa el mismo FormRequest, obtén la misma validación, consistencia garantizada.

La testabilidad es donde los FormRequests brillan de verdad. Puedes probar tu validación de forma aislada, sin pasar por el ciclo HTTP completo. Escribe una prueba que verifique que la validación falla correctamente cuando falta name, cuando domain supera el límite de caracteres, cuando el formato es incorrecto. Esas pruebas corren rápido porque no levantan tu aplicación entera. Tu equipo puede modificar reglas con confianza sabiendo que las pruebas le cubren.

La localización es automática. Tu método messages() devuelve claves de traducción como 'validation.name.required'. Laravel las busca en tus ficheros de idioma (lang/en/validation.php, lang/es/validation.php, etc.) y devuelve el mensaje correcto para el idioma del usuario. Tu API soporta al instante errores de validación multilingües sin cableado extra. Un consumidor en español recibe mensajes en español. Uno en francés, en francés. Un sistema, infinitos idiomas.

Las funcionalidades de seguridad vienen horneadas. Los tokens CSRF se validan automáticamente en peticiones que cambian estado (POST, PUT, DELETE). No tienes que acordarte de añadir comprobaciones en todas partes. Laravel ofrece más regalos: el método authorize() te deja añadir lógica de autorización dentro del propio FormRequest. El método prepareForValidation() te deja sanear la entrada antes de validar. Estas seguridades son valores por defecto: se aplican salvo que las desactives explícitamente, que es lo contrario de cómo construye la mayoría (seguridad como ocurrencia tardía).

La autorización puede ocurrir en el propio FormRequest mediante authorize(). No todos los usuarios deberían poder crear licencias. Quizá sólo administradores, o sólo usuarios con cierto rol. En lugar de esparcir comprobaciones por tu base de código, resuélvelas aquí. Puedes comprobar permisos, verificar el estado del usuario, validar relaciones —todo antes de que corra la lógica de tu controlador. Si la autorización falla, la petición falla en la frontera con un 403 claro. Tu controlador nunca tiene que preocuparse de peticiones no autorizadas.

Valida los datos en la frontera. Una vez pasado el controlador, asume que son válidos.

El FormRequest es tu primera línea de defensa contra los datos malos.