Ember.js POST requests return 400 from the server (Grape API) but are stored successfully into local storage

922 views Asked by At

I've been trying to get a simple Ember.js application to post to a Grape API backend for hours now, but I cannot seem to get it to work. I know the API works because I can post new records to it through the Swagger documentation, and they are persisted. I know the API and Ember are talking just fine because I can get all records from the server and interact with them on the page, and I know that Ember is working fine in a vacuum because my records are persisted to local storage.

However, I just cannot seem to get a POST request to work. It always comes back a 400. I have Rack-Cors configured properly, and I have everything set up with an ActiveModelAdapter on the Front-End and an ActiveModelSerializer on the back end.

Here is the model

Contact = DS.Model.extend {
  firstName: DS.attr('string'),
  lastName:  DS.attr('string'),
  email:     DS.attr('string'),
  title:     DS.attr('string'),
  createdAt: DS.attr('date'),
  updatedAt: DS.attr('date')
}

and Controller

ContactsNewController = Ember.ObjectController.extend(
  actions:
    save: ->
      @get('model').save()
    cancel: ->
      true
)

The relevant part of the API looks like this

desc 'Post a contact'
  params do
    requires :first_name, type: String, desc: 'First name of contact'
    requires :last_name , type: String, desc: 'Last name of the contact'
    requires :email     , type: String, desc: 'Email of the contact'
    requires :title     , type: String, desc: 'Title of the contact'
  end
  post do
    Contact.create!(
      first_name: params[:first_name],
      last_name:  params[:last_name],
      email:      params[:email],
      title:      params[:title]
    )
  end

The form I'm using is...

<form {{action 'save' on='submit'}}>
  <h2>{{errorMessage}}</h2>

  <label>First Name</label>
  {{input value=firstName placeholder='First Name' type='text' autofocus=true}}

  <label>Last Name</label>
  {{input value=lastName placeholder='Last Name' type='text'}}

  <label>Email</label>
  {{input value=email placeholder='Email' type='text'}}

  <label>Job Title</label>
  {{input value=title placeholder='Job Title' type='text'}}

  <hr>

  <input type='submit' value='Save'>
</form>

And the response I get is...

{"error":"first_name is missing, last_name is missing, email is missing, title is missing"}

Any takers? Sorry, I'm new to this. Something isn't getting bound, but I don't know why.


UPDATE

More investigations...

IMAGE: POST requests to the API cURL (via Postman) work just fine..

However, when I POST from Ember, the server response is still

{"error":"first_name is missing, last_name is missing, email is missing, title is missing"}

IMAGE: The output from the POST request from the Chrome Dev Tools looks like this

I have also changed the controller to..., which gives me the output in the chrome dev tools log above.

`import Ember from 'ember'`

ContactsNewController = Ember.ObjectController.extend(
  actions:
    createContact: ->
      firstName = @get('firstName')
      lastName  = @get('lastName')
      email     = @get('email')
      title     = @get('title')

    newRecord = @store.createRecord('contact', {
                   firstName: firstName,
                   lastName:  lastName,
                   email:     email,
                   title:     title
                 })

    self = this

    transitionToContact = (contact) ->
      self.transitionToRoute('contacts.show', contact)

    newRecord.save().then(transitionToContact)
   )

`export default ContactsNewController`
2

There are 2 answers

4
Marlon On BEST ANSWER

I know absolutely nothing about Ember.js but I've been building APIs using Grape for a while so I think I can help you. Looking the image that you've attached, it seems that Ember.js is creating an incorrect JSON inside the Payload and your Grape API isn't expecting a JSON formmated that way. As you can see in the second image, it's creating a JSON like this:

{ contact: {...} }

However, your API is expecting a JSON format like this one:

{ first_name: "" ... }

Now, look how you're sending the same request through the Chrome Dev Tools... you're using the "form-data" option to create the body request and that's why in this specific case it's working. Try changing it to "raw" and putting the incorrect JSON above and you'll get the same error that you're getting using Ember.Js.

I don't have the specific solution because I don't have expertise to assist you with Ember.Js... but you have to change something in your Amber.js application in a way that it creates a JSON request like this:

{ first_name: "", last_name: "", email: "" }

Instead of:

{ contact: { first_name: "" ... } }

Hope it helps you!

UPDATE

Another solution to your problem is to change your Grape API. In this case, you have to create a group block inside your params block, like this (take into account that the contact fields are now stored inside the params[:contact] Hash):

desc 'Post a contact'
params do
    group :contact, type: Hash do
        requires :first_name, type: String, desc: 'First name of contact'
        requires :last_name , type: String, desc: 'Last name of the contact'
        requires :email     , type: String, desc: 'Email of the contact'
        requires :title     , type: String, desc: 'Title of the contact'
    end
end
post do
    Contact.create!(
        first_name: params[:contact][:first_name],
        last_name:  params[:contact][:last_name],
        email:      params[:contact][:email],
        title:      params[:contact][:title]
    )
end
0
null On

If you want to change the way Ember format the JSON you will need to create a custom serializer and override the serializeIntoHash function. You can use this method to customize the root keys serialized into the JSON. By default the REST Serializer sends the typeKey of a model, which is a camelized version of the name.

Ember-cli comes with a generator for starting serializers. You can run it with:

 ember g serializer Contact

Out of the box, the serializer will look like this:

import DS from 'ember-data';

export default DS.RESTSerializer.extend({
});

To make it work with grape you can do this:

import DS from 'ember-data';

export default DS.RESTSerializer.extend({
  serializeIntoHash: function(data, type, record, options) {
    var properties = this.serialize(record, options);
    for(var prop in properties){
      if(properties.hasOwnProperty(prop)){
        data[prop] = properties[prop];
      }
    }
  }
});

More info in the documentation.