API design/routing with nested resources

Hey all!

I’m just curious, how would you surface the ability to create nested items in an API? Say you have a model Post that has_many observations.

I have my base routes for both Post and Observations, but in thinking about a client using the API, they’d probably need the UI to have the post form and observation forms in the same view since observations are directly tied to posts. One problem is that when you’re adding a post, it’s new so we can’t just create an observation by sending a POST to /api/v1/observations because we don’t have a post_id to assign to yet. I suppose someone making the client (my future self) could force the creation of a post before adding observations to it, but that feels more like a limitation than anything.

Hey @seanwash,

Great question! Have you looked into accepts_nested_attributes_for yet?

The purpose of accepts_nested_attributes_for is to allow child models (Observation) to be created at the same time as a parent model (Post), which sounds exactly like your use case. You do this by including a collection of child attributes along with the attributes for the parent model in a request to the parent model’s create action.

There are a few steps to enable this behavior.

  1. Add accepts_nested_attributes_for to your Post model

    class Post < ActiveRecord::Base
      has_many :observations
    
      # add this =>
      accepts_nested_attributes_for :observations
    end
    
  2. Update Strong Parameters to allow the nested params

    You need to let your controller know to accept the attributes that will be coming in for the nested model.

    class PostsController
      # ...
      
      def post_params
        params.require(:post).permit(
          ... 
          observations_attributes: [:attr1, :attr2]
      end
    end
    

    observation_attributes is a hash key whose value is an array of all the attributes of the Observation which should be allowed.

  3. Important Add inverse_of to the Post model

    Here is where people get tripped up. As you pointed out above, an Observation has a foreign key to a Post, but since we are creating both types of records at the same time, we won’t yet have a post_id to associate with.

    The inverse_of option sets up a relationship between the parent/child association and lets the Post be created before the Observation objects and then link them together. Without this, if you try sending a request using AFAF, you’ll get back an error "observations.post":["must exist"].

    class Post < ActiveRecord::Base
      has_many :observations, inverse_of: :post
    end
    

    Read more about inverse_of.

  4. Send a request

    Now all you need to do is send a POST request to the /posts endpoint and include parameters for the Post and its nested Observations

    // Sample params for JSON request
    {
       "post": {
          "title": "Foo"
          "content": "Bar",
          "observations_attributes": [
             { "attr1": "Baz 1", "attr2": "Buzz 1" },
             { "attr1": "Baz 2", "attr2": "Buzz 2" }
          ]
       }
    }
    

    or

    curl -i -H "Content-Type: application/json" -H "Accept: application/json" -X POST http://dev.carlosramireziii.com:3000/articles -d "post[title]='Foo'&post[content]='Bar'&post[observations_attributes][0][attr1]='Baz'&post[observations_attributes][0][attr2]='Buzz'"
    

Hope that helps!

That makes total sense!