3

I have an endpoint returning single-element collection (I didn't return just object-instance to keep consistent with resource-as-collection convention, so only get-by-id returns single instance)

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
    }
]

And the new requirement showed up: some devices are connected into pairs and while searching for device 12345 which is paired with device 78901 I need to retrieve both of those (preferably in single HTTP call). What are my best options? I tried with:

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
    },
     {
        "id": 2, 
        "serialNumber": "78901"
    }
]

But this breaks the semantics (I filter devices rresoure "list" for one S/N and suddenly another device with different S/N pops up)

Then I tried this:

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
        "connected": 
        {
            "id": 2, 
            "serialNumber": "78901"
        }
    }
]

But still recursively-nested resource doesn't feel right since this is totally different from how the domain model is expressed. Is there any better way to design this endpoint without exposing the details of how they are connected? There is some business logic which is pretty complicated and irrelevant to the endpoint consumer, she only needs to know whether there is a paired device or not.

  • It would help a little bit if you expose how these 2 resources are connected IN the domain model. You don't have to expose them, just show us to make the question easier to understand. Are those pairs exclusive (if A pairs with B, B only has one pair, and it's with A) ? Are they infinitely nested ? – Machado Jan 23 '17 at 16:58
  • Yes, the pairs are exclusive (if device A is paired with device B, that means neither dev A nor dev B can be paired with any other device, so as a consequence there would be always only one level of nesting) – AndrzejD Jan 24 '17 at 10:38
4

But still recursively-nested resource doesn't feel right since this is totally different from how the domain model is expressed.

There's no good reason to strongly couple your API resource representations to your domain model. They serve different clients with different needs, and their design paradigms are totally different.

I would suggest that you add support for a query parameter ?includeConnectedDevices=true. Then you can do

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
    }
]

GET /devices?serialNumber=12345&includeConnectedDevices=true
[
    {
        "id": 1, 
        "serialNumber": "12345",
        "connected": 
        {
            "id": 2, 
            "serialNumber": "78901"
        }
    }
]

Clients can choose if they care about the connected device(s). As a bonus, you're able to an arbitrary number connections.

Whatever you do, you should consider including a link (or links) from a device to it's connected device(s). That might be in a header, or as an attribute of the entity, or maybe you wrap your responses in an envelope. Following links is much nicer than building URLs. That might look like /devices?connectedTo=12345.

  • +1 for 'api model is not bound to domain model' – marstato Jan 23 '17 at 16:17
  • "Clients can choose if they care about the connected device(s)." This is and internal API and we made a design decision that API endpoint serve one and only one client and the client explicitely states whether she care about the connected device. But this is a bit off-topic, in general terms your comment is perfectly valid. – AndrzejD Jan 24 '17 at 10:42
0

You can use HATEOAS to link your resources so instead of returning this:

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
    },
    {
        "id": 2, 
        "serialNumber": "78901"
    }
]

you can return something like this:

GET /devices?serialNumber=12345
    {
        "id": 1, 
        "serialNumber": "12345",
        "links":[
            {
               rel:"connected",
               "id":2,
               "serialNumber": "78901"
            }
        ]
    }

UPDATE

@Ewan is totally right, it should be something like:

 GET /devices?serialNumber=12345
    {
        "id": 1, 
        "serialNumber": "12345",
        "links":[
            {
               "rel":"connected",
               "href":"http://yourserver.com/yourapp/devices?serialNumber=78901"
            }
        ]
    }

** SECOND UPDATE** (after having my morning coffee edition)

If you take a look at the comments you'll see that while the original answer complies with all your requirements (I'm with @Eric-Stein on the "this should not require a single HTTP request") the first update is the "standard" (at least one of them) approach. IMHO you should go with HATEOAS or HAL, or some other well known alternative instead of doing the nested resources thing.

  • surely the hateos link should be another url? – Ewan Jan 23 '17 at 14:15
  • @Ewan: You're right, fixing the answer – yorodm Jan 23 '17 at 14:38
  • "preferably in a single HTTP call". While I disagree with it as a requirement given the problem statement as is, it is an optional requirement that you're not addressing at all. – Eric Stein Jan 23 '17 at 14:46
0

new requirement showed up: some devices are connected into pairs and while searching for device 12345 which is paired with device 78901 I need to retrieve both of those (preferably in single HTTP call). What are my best options?

That's going to depend on your constraints.

One approach is very simple: recognize that the new requirement describes a new view of device 12345. That is to say, it describes a new document, which is another way of saying it describes a new resource.

So figure out a name for this new report, and then use that name with your URI spelling standards to figure out a reasonable approach.

GET /devicePairs?eitherDevice=12345 ...

Note: Eric's suggestion of adding another parameter to the query string is a example of this approach -- inventing a new URI to cover the new use case.

On the other hand, if the new requirement is that you extend the existing representation to include new information when available, then you don't need to mint a new URI, but you should extend the existing representation with the additional data.

That, in turn, depends on having a media type that supports extensions. The easy part is putting in the data; the harder part is ensuring that existing clients don't break.

The basic contract that you need in place is the notion of optional fields in your representation, combined with the notion that consumers must ignore fields that they don't recognize. The Atom Syndication Specification is a good reference implementation of this idea.

In plain old JSON, this might look like

{
    "id": 1, 
    "serialNumber": "12345",
    "connectedTo" : "78901"
}

Or you might instead prefer to make the notion of extensions explicit in your representation

{
    "id": 1, 
    "serialNumber": "12345",
     extensions : {
        "connectedTo" : "78901"
     }
}

There are lots of different ways that you might spell this, but what it really amounts to is ensuring that your representations are backwards compatible.

If your solution were employing hypermedia, you might provide the extensions with links.

{
    "id": 1, 
    "serialNumber": "12345",
     connectedTo : {
        "serialNumber" : "78901",
        "href" : "/devices?serialNumber=78901"
     }
}   

Links are a natural fit for extension points, so you might invert this

{
    "id": 1, 
    "serialNumber": "12345",
     links : [
         {
             rel : "connectedTo",
             href : "/devices/78901"
             serialNumber : "78901"
         }
     ]
} 

Keep wiggling the representation, and you eventually end up with something like HAL or JSON API

0

Plenty of approaches were described already, but I believe there are only 2 reasonable options (extremes) with everything else being a half-measure between the two.

  1. Use a query param that describes a common attribute "unique" to both devices and return them in an array. eitherSerialNumber sounds awkward:

    GET /devices?eitherSerialNumber=12345
    [
        {
            "id": 1, 
            "serialNumber": "12345"
        },
         {
            "id": 2, 
            "serialNumber": "78901"
        }
    ]

  1. Use a full blown envelope with the main and linked resource as well as the links (optionally in both) flattened. This is what JSON:API and some other standard proposals do:

     GET /devices?serialNumber=12345
     {
         "data": {
             "id": 1, 
             "serialNumber": "12345",
             "related": {
                  "connectedDevice": {
                      "type": "device",
                      "id": 2
                  }
             }
         },
         included: [{
             "type": "device",
             "id": 2, 
             "serialNumber": "78901"
         }]
     }

The first option is simple and still semantically correct. The second one ultimately solves the problem of serializing graphs of resources while introducing some overhead.

I would recommend against choosing any middle ground, as it might work today, but won't be sufficient tomorrow and you'll end up asking a follow up question here again.

  • If you consider a device to be connected to itself, then you can use "connectedTo" for (1). – Eric Stein Jan 24 '17 at 14:51

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.