30

I'm designing a pragmatic REST API and I'm a little stuck on how best to add existing entities to a collection. My domain model includes a Project that has a collection of Sites. This is a strict many-to-many relationship and I have no need to create an entity that explicitly models the relationship (i.e. ProjectSite).

My API will allow consumers to add an existing Site to a Project. Where I'm getting hung up is that the only data I really need are ProjectId and SiteId. My initial idea was:

1. POST myapi/projects/{projectId}/sites/{siteId}

But I also thought about

2. POST myapi/projects/{projectId}/sites

with a Site entity sent as JSON content.

Option 1 is simple and works but doesn't feel quite right, and I have other relationships that cannot follow this pattern so it adds inconsistency to my API.

Option 2 feels better but leads to two concerns:

  • Should I create a Site or throw an exception if a new Site is posted (SiteId = 0)?
  • Because I only need ProjectId and SiteId to create the relationship, the Site could be posted with wrong or missing data for other properties.

A 3rd option is to provide a simple endpoint solely for creating and deleting the relationship. This endpoint would expect a JSON payload containing just ProjectId and SiteId.

What do you think?

3
  • 2
    See also: stackoverflow.com/questions/2001773/… Mar 12 '14 at 15:07
  • @RoryHunter There's some interesting discussion in that link but nothing that eliminates my uncertainty. I especially like that the accepted answer states "You understood right." and the 2nd place (albeit by a large margin) answer "Simply put, you are doing this completely backward."
    – Jamie Ide
    Mar 12 '14 at 15:18
  • Your first option is fine though I would use PUT instead of POST as the client is in control of the identity being added to the collection. Your first concern with option 2 is entirely up to you, if you don't want new sites, don't throw an exception but return one of the 4xx codes. Your second concern is neither here nor there. You shouldn't be posting an entire Site anyway unless you allow additions. Adding an existing site should have the id only as you are modifying the site but only the "ProjectSite" collection (even if you don't create a separate resource for it). Mar 12 '14 at 17:39
20

POST is the "append" verb, and also the "processing" verb. PUT is the "create/update" verb (for known identifiers), and almost looks like the right choice here, because the full target URI is known. projectId and siteId already exist, so you don't need to "POST to a collection" to produce a new ID.

The problem with PUT is that it requires the body be the representation of the resource you're PUTting. But the intent here is to append to the "project/sites" collection resource, rather than updating the Site resource.

What if someone PUTs a full JSON representation of an existing Site? Should you update the collection and update the object? You could support that, but it sounds like that's not the intent. As you said,

the only data I really need are ProjectId and SiteId

Rather, I'd try POSTing the siteId to the collection, and rely on the "append" and "process" nature of POST:

POST myapi/projects/{projectId}/sites

{'id': '...' }

Since you're modifying the sites collection resource and not the Site resource, that's the URI you want. POST can know to "append/process" and add the element with that id to the project's sites collection.

That still leaves the door open to creating brand new sites for the project by fleshing out the JSON and omitting the id. "No id" == "create from scratch". But if the collection URI gets an id and nothing else, it's pretty clear what needs to happen.

Interesting question. :)

2
  • I'm of the faith that believes POST is for create and PUT is for update but your conclusion is where I ended up yesterday. The nice thing is that thanks to attribute routing in Web API, I have the code in a ProjectSites controller so the code is well organized.
    – Jamie Ide
    Mar 13 '14 at 15:46
  • I think the defining reason you need to use POST instead of PUT or PATCH here is that you don't have the entire Site entity to put into the sites resource. You only have the id, which requires processing for it to add it to the collection.
    – crush
    Feb 22 '19 at 16:48
4

We use the Patch method for stuff like this. What you want to do is modify an existing Project to add a Site to it.

So something like this would work

PATCH myapi/projects/{id} 

with the Site(s) entity as JSON/JSONArray in the request body.

That way you can use the same URL to modify different parts of the Project if you needed to -- your code in the implementation must be intelligent enough to handle this partial modification of the resource.

5
  • Interesting approach. I have a "rich" (i.e. highly dependent) older domain model and Project especially has many collections hanging off it. Detecting the entity type being in the request would be a challenge and doesn't fit in with my pragmatic goal.
    – Jamie Ide
    Mar 12 '14 at 17:27
  • Why a challenge? If you have those restrictions, you can always use a JSON that makes explicit what it's sending... like {"sites": [], "other-stuff": {}}, you can then branch your code to handle all those "subjsons" very easily. It really depends on your specific problem, but I would still recommend using PATCH as it's designed specifically for these kind of things.
    – juan
    Mar 12 '14 at 18:37
  • The downsides I see are 1) API doesn't explicitly communicate which collections allow changes; 2) can't leverage Web API parameter binding; 3) big switch or if statement.
    – Jamie Ide
    Mar 12 '14 at 18:58
  • I've never seen the patch method used anywhere else Mar 13 '14 at 7:42
  • 1
    Wouldn't PATCH also expect the full entity to be passed as its value here, rather than an id that points to some entity?
    – crush
    Feb 22 '19 at 16:46
0

Suppose you have an job object

{
  "job": {
   
    "id": 1,

    "tasks": [
      {
         "id": 123,
         "name": "play"
      },
      {
         "id": 456,
         "name": "jump"
      }
     ]
   }
}

To add a new task use

POST /job/1/task
{
   "name": "work"
}

To add multiple tasks use

POST /job/1/task
[
  {
     "name": "work"
  },
  {
     "name": "work"
  }
]

If the client generates the id use

POST /job/1/task
{
   "name": "work",
   "id": "6d212299-c6b5-477c-bb04-5560a7515cae"
}

To overwrite an existing task use

PUT /job/1/task/456
{
   "name": "sleep"
}

To overwrite all tasks use

PUT /job/1/task
[ 
  {
     "name": "slap"
  },
  {
     "name": "kiss"
  }
]

To delete a task use

DELETE /job/1/task/456

To delete multiple tasks use

DELETE /job/1/task?ids=123&ids=456

Suppose your data is normalized so that tasks property just stores the task IDs

{
  "job": {
   
    "id": 1,

    "tasks": [
      123,
      456
     ]
   }
}

To link a task to a job use

POST /job/1/task
123

To link multiple task to a job use

POST /job/1/task
[
  123,
  456
]

To overwrite all task links use

PUT /job/1/task
[
  123,
  456
]

To unlink a task from a job use

DELETE /job/1/task?id=123

To unlink multiple tasks from a job use

DELETE /job/1/task?ids=123&ids=456

To overwrite a task use

PUT /task/123
{ 
  "name": "cry"
}

To delete a task use

DELETE /task/123

To delete multiple tasks use

DELETE /task?ids=123&ids=456

If tasks is a map

"tasks": {
   "abc": {
       "name": "sing"
    },
    "def": {
       "name": "play"
    }
}

To add a task

PATCH /job/1/tasks
{ 
  "xyz": {
     "name": "drink"
  }
}

To delete a task

DELETE /job/1/tasks/xyz

To delete multiple tasks

DELETE /job/1/tasks?keys=hij&keys=xyz

To overwrite all the tasks

PUT /job/1/tasks
{
   "uyt": {
       "name": "type"
    },
    "opl": {
       "name": "swing"
    }
}

There are subtle differences in each operation and vary based on the data model.

Please refer to the examples on "Linking a task to a job" for the answer to your question. I have provided all the other examples so that you can see why you would NOT use any other way of doing it. Those other http methods and strategies are used for different things.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.