Reputation: 6914
(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.
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.)
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 |
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.)
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
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:
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;
}
// ...
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;
}
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