Using Laravel's Form Request Validation With A Json Api Endpoint Enter a brief summary for this post

Using Laravel's Form Request Validation with a Json API Endpoint

I like to separate my form validation from the controller when it's an API endpoint. The validation would use the Validator facade like so, below.

// a Service layer
public function create(array $data)
	{
		$validator = Validator::make($data, [/* rules */], $messages=[]);

        if($validator->fails())
        {
        	return $validator->messages()->get('*');
        }

        try
        {
	        return $this->getRepository()->create($data);
	    }
	    catch(TransactionFaultException $e)
	    {
	    	throw new ServiceFaultException($e->getMessage());
	    }
	}

I use a Service with a Repository as the layered architecture improves code readability and maintenance. So the validation is on the Service but still it feels uncomfortable. Is there a way to separate this validation out of the Service (or Controller were you not to use a separate layer) when making API calls?

Yes. But prior to using the following solution below my validation would fail with no errors being returned to the client. Worst case was a 500 status code.

Simply run the command php artisan make:request UserLoginRequest as you normally would. This example is for a user login endpoint but it works for any validation with form data you care to do. When you modify your UserLoginRequest implementation to include the rules and messages, also include the following two namespaces to the top of the file and add the protected class method to the end.

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

// ... etc ...

protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(response()->json($validator->errors(), 422));
    }

And use Postman to make a request with missing or ill formed data. You'll get the JSON response you'd want for the client to deal with ― as if you lumbered the validation in the Service or Controller. Of course any unit tests need to be refactored too, and those I had from before now look like what is below.

class AuthenticateControllerTest extends TestCase
{
    use RefreshDatabase;

    protected string $endpoint='/api/v1';

    public function setUp() : void
    {
        parent::setUp();
    }

    public function tearDown() : void
    {
        parent::tearDown();
    }

    /** @test **/
    public function user_registration_required_fields() : void
    {
        $this->withExceptionHandling();

        $response=$this->json('POST', $this->endpoint.'/authenticate/register', [], [
            'Accept'=>'application/json'
        ]);
        
        $response->assertStatus(422);
        $response->assertJsonStructure([
            'name',
            'email',
            'password',
        ]);
    }

    /** @test **/
    public function can_a_new_user_register() : void
    {
        $this->withExceptionHandling();

        $user=User::factory()->make();
        $response=$this->json('POST', $this->endpoint.'/authenticate/register', [
            'name'=>$user->name,
            'email'=>$user->email,
            'password'=>'password'
        ], ['Accept'=>'application/json']);
        
        $response->assertStatus(201);
        $response->assertJsonStructure([
            'status'
          , 'user'=>[
                'id'
              , 'name'
              , 'email'
            ]
        ]);

        $this->assertDatabaseHas('users', [
            'name'=>$user->name,
            'email'=>$user->email
        ]);
    }

    /** @test **/
    public function user_login_required_fields() : void
    {
        $this->withExceptionHandling();

        $response=$this->json('POST', $this->endpoint.'/authenticate/login', [], [
            'Accept'=>'application/json'
        ]);
        
        $response->assertStatus(422);
        $response->assertJsonStructure([
            'email',
            'password',
        ]);
    }

    /** @test **/
    public function can_a_registered_user_login() : void
    {
        $this->withExceptionHandling();

        $user=User::factory()->create(); 
        $response=$this->json('POST', $this->endpoint.'/authenticate/login', [
            'email'=>$user->email,
            'password'=>'password'
        ], ['Accept'=>'application/json']);
        
        $this->assertAuthenticated('api');

        $response->assertStatus(200);
        $response->assertJsonStructure([
            'status',
            'token',
            'token_type',
            'expiry',
            'user'=>[
                'id',
                'name',
                'email',
            ]
        ]);
    }

    /** @test **/
    public function a_fake_user_cannot_login() : void
    {
        $this->withExceptionHandling();

        $response=$this->json('POST', $this->endpoint.'/authenticate/login', [
            'email'=>'fake@bt.com'
          , 'password'=>'password'
        ], ['Accept'=>'application/json']);
        
        $this->assertGuest('api');

        $response->assertStatus(401);
        $response->assertJsonStructure([
            'status'
        ]);

        $content=json_decode($response->getContent(), true);

        $this->assertTrue($content['status'] === false);
    }

    /** @test **/
    public function can_an_authenticated_user_logout() : void
    {
        $this->withExceptionHandling();

        $user=User::factory()->create(); 
        $response=$this->json('POST', $this->endpoint.'/authenticate/login', [
            'email'=>$user->email
          , 'password'=>'password'
        ], ['Accept'=>'application/json'])
        ->assertStatus(200)
        ->assertJsonStructure([
            'status',
            'token'
        ]);
        
        $this->assertAuthenticated('api');

        $response=$this->json('POST', $this->endpoint.'/authenticate/logout', [], ['Accept'=>'application/json']);
        $response->assertStatus(200)
            ->assertJsonStructure([
                'status'
              , 'message'
            ]);

        $this->assertGuest('api');
    }

    /** @test **/
    public function can_a_user_refresh_their_token() : void
    {
        $this->withExceptionHandling();

        $user=User::factory()->create(); 
        $response=$this->json('POST', $this->endpoint.'/authenticate/login', [
            'email'=>$user->email
          , 'password'=>'password'
        ], ['Accept'=>'application/json']);
        
        $this->assertAuthenticated('api');

        $response->assertStatus(200);
        $response->assertJsonStructure([
            'status'
          , 'token'
          , 'token_type'
          , 'expiry'
          , 'user'=>[
                'id'
              , 'name'
              , 'email'
            ]
        ]);

        $content=json_decode($response->getContent(), true);
        $response=$this->json('POST', $this->endpoint.'/authenticate/refresh', [
            'email'=>$user->email
          , 'password'=>'password'
        ], [
            'Accept'=>'application/json', 
            'Authorization'=>'Bearer '.$content['token']
        ]);
        
        $response->assertStatus(200);
        $response->assertJsonStructure([
            'status'
          , 'token'
          , 'token_type'
          , 'expiry'
          , 'user'=>[
                'id'
              , 'name'
              , 'email'
            ]
        ]);
    }

    /** @test **/
    public function a_registered_user_cannot_register_again_using_the_same_email_address() : void
    {
        $this->withExceptionHandling();

        $user=User::factory()->make(); 
        $response=$this->json('POST', $this->endpoint.'/authenticate/register', [
            'name'=>$user->name,
            'email'=>$user->email,
            'password'=>'password'
        ], ['Accept'=>'application/json']);
        
        $response->assertStatus(201);
        $response->assertJsonStructure([
            'status',
            'message',
            'user'=>[
                'id',
                'name',
                'email',
            ]
        ]);
        
        $this->assertDatabaseHas('users', [
            'name'=>$user->name
          , 'email'=>$user->email
        ]);

        $response=$this->json('POST', $this->endpoint.'/authenticate/register', [
            'name'=>$user->name,
            'email'=>$user->email,
            'password'=>'password'
        ], ['Accept'=>'application/json']);
        
        $response->assertStatus(422);
        $response->assertJsonStructure([
            'email',
        ]);
    }
}

Gone are the 409 Conflict status replaced with 422 Unprocessable Entity status for a response. All green once again. This is a huge boost to maintainable (more testable) code. The old argument of adding more layers adds complexity in software. That's not true because the complexity arises from unreadable (unmaintainable) spaghetti code.