Jochem Schulenklopper
Jochem Schulenklopper

Reputation: 6914

How to specify subtypes of some resource type in a RESTful API?

(Couldn't find a tutorial or clear description of this in guides like REST API Tutorial and StackOverflow's blog on REST API best practices, and a SO question on subtypes discusses another angle.)

How should we specify different subtypes of the same resource type in a REST API?

For example consider different kinds of contacts in a CRM application: B2C customers which are persons, B2B customers who are companies, and B2E persons which are employees of our own company.

I can think of two ways of specifying that in the URL path. The main distinction is how it affects the list of required/optional resource attributes in the documentation. Similarly, this oozes through in the code handling the API requests. The validation whether some attributes are allowed, required or optional likely differs between entities of different resource types.

Specify subtypes explicitly in the resource URL path

https://api.example.com/contacts/b2b/:id
https://api.example.com/contacts/b2c/:id
https://api.example.com/contacts/b2e/:id

or something like

https://api.example.com/b2b-contacts/:id
https://api.example.com/b2c-contacts/:id
https://api.example.com/b2e-contacts/:id\

The advantage is that the API contract can be defined very specifically, and tailored to each type of the resource. Some attributes are common (id,name,street,email,...) whereas other attributes are specific only to one type (coc, duns, vat-id, ... for B2B contacts, and employee-id, department, dob, ... for employees). The API documentation could clearly specify the list of allowed/required/optional attributes for each subtype/resource.

(Don't dive into the proper way to register company contacts or employees... it's just an example.)

Group different subtypes under the common resource in the URL path

https://api.example.com/contacts/:id
with the objects (resource state representations conforming to the REST parlance) specifying the subtype in one of the attributes being exchanged.

This is more generic and flexible, but the API documentation needs to clearly specify which attributes are required/optional for each contact type. type will be an attribute too.

So, in this case, the documentation is likely to list attributes like

Attribute Applicable for types Required
id All (or: B2B, B2C, B2E) Yes
name All Yes
email All No
contact-type All Yes
... ... ...
... ... ...
employee-id B2E Yes
dob B2E, B2C No
dept B2E No
... ... ...
coc B2B Yes
fax B2B, B2C No
... ... ...
ssn B2C Yes

(Again, don't critique the specifics and appropriateness of this CRM example. It's just an example. I know we shouldn't store a social security number commonly, and B2B customers typically contain/refer to a person as well.)

Question

What's the common way to handle this? How to document it, and how should attribute validations (allowed, required, optional) be handled?

Upvotes: 1

Views: 1329

Answers (1)

JJ Geewax
JJ Geewax

Reputation: 10579

Ah good old API resource polymorphism... This is a tricky one :-)

In general, I'd say the answer to this question depends a bit on how the API is going to be used. A good test for this is how the users of the API want to list the "contacts". Will users want to "list all contacts"? Or will they want to "list all B2B contacts" and never require the ability to list B2B contacts alongside B2E contacts?

If you isolate these into separate resources (e.g., B2BContact, B2EContact, etc), then there's no way for users to browse through all the contacts (of any type). However they can still browse contacts if combined into a single type (e.g., GET /contacts?filter=type:B2B or something similar).

If the different types of contacts (B2B / B2E / B2C) are all truly fundamentally different, it'd be a mistake to combine them into a single resource type. Right now you have some properties that apply only to specific types but not all -- however that's what exists today. It's far more likely that over time the number of properties will grow, and you'll have quite a bit of confusion about things that made sense together when the API started but not so much anymore, which can be very frustrating for users.

Onto the specifics...

If you're going to separate these into multiple resource types, I'd suggest treating them as simply separate resources (GET /b2b-contacts/, GET /b2c-contacts/, etc) and not separate types of the same resource (e.g., GET /contacts/b2b). This way, if they need to deviate from another in the future, there's nothing tying the resources together because "they all happened to be contacts when the API started".

If you're going to combine these as a single resource type with a type field to indicate the B2B/B2C/B2E aspect:

  1. For fields that only apply for specific types, try putting them into a "container" for data of that type. For example, fax only applies to B2B type contacts, so (using Typescript here, sorry if that's not your jam):
class Contact {
  id: string;
  type: 'b2b' | 'b2c' | 'b2e';
  // ...

  metadata: B2BContactMetadata | B2CContactMetadata | ...;
}

class B2BContactMetadata {
  fax: string;
}

// ...
  1. The same thing applies for fields that apply to 2 of the 3 types. If fax suddenly makes sense for B2B and B2E but not B2C, you'd simply add it to the B2EContactMetadata:
class B2BContactMetadata {
  fax: string;
}

class B2EContactMetadata {
  fax: string;
}
  1. Be very careful about hoisting fields up into the Contact-level. If something is truly fundamental to all current and future contacts, then it belongs at that top level. If it just happens to apply to all contact types you have now, but there's a chance it wouldn't make sense for a new type, then put it in the type-specific metadata area (or one of them).

For more exploration on this topic, Chapter 16 of API Design Patterns covers Polymorphism in APIs (https://livebook.manning.com/book/api-design-patterns/chapter-16)

Upvotes: 2

Related Questions