What causes the data validation error in this Laravel API? [duplicate]

I am working on a registration form with Laravel 8 and Vue 3. The back-end is an API.

In the users table migration file I have:

class CreateUsersTable extends Migration {

 public function up()
 {
  Schema::create('users', function (Blueprint $table) {
      $table->id();
      $table->string('first_name');
      $table->string('last_name');
      $table->string('email')->unique();
      $table->timestamp('email_verified_at')->nullable();
      $table->string('password');
      $table->unsignedInteger('country_id')->nullable();
      $table->foreign('country_id')->references('id')->on('countries');
      $table->rememberToken();
      $table->timestamps();
   });
 }
 // More code here
}

As can be seen above, the id in the countries table is a foreign key in the users table.

I have this piece of code in the AuthController to register a new user:

class AuthController extends Controller {
    
    public function countries()
    {
        return country::all('id', 'name', 'code');
    }
    
    public function register(Request $request) {

        $rules = [
            'first_name' => 'required|string,',
            'last_name' => 'required|string',
            'email' => 'required|email|unique:users,email',
            'password' => 'required|string|confirmed',
            'country_id' => 'required|exists:countries',
            'accept' => 'accepted',
        ];

        $customMessages = [
            'first_name.required' => 'First name is required.',
            'last_name.required' => 'Last name is required.',
            'email.required' => 'A valid email is required.',
            'email.email' => 'The email address you provided is not valid.',
            'password.required' => 'A password is required.',
            'password.confirmed' => 'The passwords do NOT match.',
            'country_id.required' => 'Please choose a country.',
            'accept.accepted' => 'You must accept the terms and conditions.'
        ];

        $fields = $request->validate($rules, $customMessages);
    
        $user = User::create([
            'first_name' => $fields['first_name'],
            'last_name' => $fields['last_name'],
            'email' => $fields['email'],
            'password' => bcrypt($fields['password']),
            'country_id' => $fields['country_id']
        ]);

        $token = $user->createToken('secret-token')->plainTextToken;

        $response = [
            'countries' => $this->countries(),
            'user' => $user,
            'token' => $token
        ];

        return response($response, 201);
    }
 }

On the front-end, I have:

const registrationForm = {
  data() {
    return {
      apiUrl: 'http://myapp.test/api',
      formSubmitted: false,
      countries: [],
      fields: {
        first_name: '',
        last_name: '',
        email: '',
        password: '',
        country_id: null,
      },
      errors: {},
    };
  },
  methods: {
    // get Countries
    async getCountries(){
      try {
        const response = await axios
          .get(`${this.apiUrl}/register`)
          .catch((error) => {
            console.log(error.response.data);
          });

        // Populate countries array
        this.countries = response.data.countries;

      } catch (error) {
        console.log(error);
      }
    },

    registerUser(){
      // Do Registrarion
      axios.post(`${this.apiUrl}/register`, this.fields).then(() =>{
        // Show success message
        this.formSubmitted = true;

        // Clear the fields
        this.fields = {}

      }).catch((error) =>{
        if (error.response.status == 422) {
          this.errors = error.response.data.errors;
        }
      });
    }
  },
  async created() {
    await this.getCountries();
  }
};

Vue.createApp(registrationForm).mount("#myForm");

In the Vue template:

<form id="myForm">
    <div v-if="formSubmitted" class="alert alert-success alert-dismissible">
      <button type="button" class="close" data-dismiss="alert">&times;</button>
      Your account was created :)
    </div>

    <div class="form-group" :class="{ 'has-error': errors.first_name }">
    <input type="text" class="form-control" placeholder="First name" v-model="fields.first_name">
    <span v-if="errors.first_name" class="error-message">{{ errors.first_name[0] }}</span>
  </div>

  <div class="form-group" :class="{ 'has-error': errors.last_name }">
    <input type="text" class="form-control" placeholder="Last name" v-model="fields.last_name">
    <span v-if="errors.last_name" class="error-message">{{ errors.last_name[0] }}</span>
  </div>

  <div class="form-group" :class="{ 'has-error': errors.email }">
    <input type="email" class="form-control" placeholder="Enter email" v-model="fields.email">
    <span v-if="errors.email" class="error-message">{{ errors.email[0] }}</span>
  </div>

  <div class="form-group" :class="{ 'has-error': errors.password }">
    <input type="password" class="form-control" placeholder="Enter password" v-model="fields.password">
    <span v-if="errors.password" class="error-message">{{ errors.password[0] }}</span>
  </div>

  <div class="form-group" :class="{ 'has-error': errors.password_confirmation }">
    <input type="password" class="form-control" placeholder="Confirm password" v-model="fields.password_confirmation">
    <span v-if="errors.password_confirmation" class="error-message">{{ errors.password_confirmation[0] }}</span>
  </div>

  <div class="form-group">
    <select class="form-control" v-model="fields.country_id">
       <option value="0">Select your country</option>
       <option v-for="country in countries" :value="country.id">{{ country.name }}</option>
    </select>
  </div>

  <div class="form-group accept pl-1" :class="{ 'has-error': errors.accept }">
    <input type="checkbox" name="accept" v-model="fields.accept">
    <p>I accept <a href="#" class="text-link">The Terms and Conditions</a></p>
    <span v-if="errors && errors.accept" class="error-message">{{ errors.accept[0] }}</span>
  </div>

  <div class="form-group mb-0">
    <button @click.prevent="registerUser" type="submit" class="btn btn-sm btn-success btn-block">Register</button>
  </div>
</form>

The problem

For a reason I was unable to figure out, the app throws this error in the browser (Network tab), with status code 422 Unprocessable Content:

The given data was invalid

Question

What am I doing wrong?


Solution 1:

The country_id field won't exist in the countries table. By default Laravel will look for the column in the database by the name of the field to be validated. So you should use this:

'country_id' => 'required|exists:countries,id'

https://laravel.com/docs/8.x/validation#specifying-a-custom-column-name