I'm working on a service that forwards/unifies our API calls to external platforms/services.
For example, let's say that I don't want to handle all possible actions (create post, retrieve post, edit post, list posts) for Facebook, Twitter, LinkedIn, Google+ at each user-facing service. So the user-facing services calls this API forwarder with a message, somehow specify the service to post to and on behalf of which user is the request coming from.
To forward the call successfully, I need to identify:
the calling service, for which I use the Authorization header.
the service to which to forward.
the user who initiated the request. I shouldn't store end user external service credentials (e.g. facebook tokens) in the calling service but in the forwarding service.
I have a few options on how to pass this data, and any input would be appreciated:
Specifiy service and user as sub-resouces in URL
/service/{externalServiceName}/user/{userReference}/Object/{objectId}
E.g.GET /service/facebook/user/uid123/posts/post345
This feels quite verbose to me.Use query parameters.
/Object/{objectId}?service={externalServiceName}&user={userReference}
E.g.POST /posts?service=facebook&user=uid123
Overload the Authorization header with:
Bearer: <OAuth token>, Service: <serviceName>, User: <userReference>
Resulting in nice URLs (GET /posts/).(削除) This seems totally out of spec though. (削除ここまで)Turns out specifying multiple values for a header separated by comma is in the spec.Use custom headers:
X-APIUnify-Service: <serviceName> X-APIUnify-User: <userReference>
Again, with nice, short URLs (POST /posts/post345
).Use a mix of custom headers and resources in URL:
GET Authorization: Bearer: <OAuth token>, User: <userReference> /{externalServiceName}/Object/{objectId}
This makes sense to me because: 1. the userReference is actually a second layer of authorization anyway. 2. The service is the resource that owns the Object. I.e. the same objectId can point to different resources in different services.Use JWT to pass the service and user, signed with the calling service secret.
JWT.payload = { "callingServiceId": "5aafcd909c9a25054c3019e3", "externalServiceName: "facebook", "userReference": "uid123", "iat": 1521638763 } Authorization: Bearer: <JWT>
Thank you!
1 Answer 1
We need to think in the best design API starting from the API objective.
Your API objective is:
service that forwards/unifies our API calls to external platforms/services
So, you start with posts
endpoint:
POST /posts/
Body
{
title: "Hello",
text: "Hello World"
}
But this post
can posted to Facebook, LinkedIn, etc and you need the user reference also.
You can pass this informations on the body, of course. But:
- Mix exclusive platform attributes on same body. The Twitter, per example, does not have a
title
or some platforms has support fortags
. This attributes will be ignored for platforms that not support them. The server will need to figure out at runtime what kind of platform he is receiving and looking if all required fields are there for that platform. The code could easily be a mess. - Client will handle the API complexity. As some attributes are exclusive for some platforms, the client needs to control what group of attributes he needs to send for each platform for the same endpoint.
- Document this API will be a difficult thing to do. Imagine put a Swagger on that. Probably the client will have a hard time trying to figure out what informations is supported for each platform.
- Support for new platforms will be harder. Maybe you would like to add a new platform that needs a new information to be passed together with the user reference (see my Slack example).
- Versioning is not possible for each platform. You will not able to version each platform individually, what could be a nice feature to follow the changes of each platform.
And etc. You can think in a lot of possibilities and particularities of each platform.
As you can see, use a single endpoint is a risk.
So seems good to me separate in different endpoints:
/platforms/facebook/
/platforms/twitter/
/platforms/google-plus/
And the URL endpoint and body
of each one can have his particularities. Divide and conquer.
You can understand this as /platforms/{platform}
, but be careful. Threat this as generic can bring some problems if you need more flexibility on the URL endpoint. Per example, to create a post on Slack you need the platform, user and a channel:
/platforms/slack/users/slav/channels/rest-questions/
Thinking in this way, you can separate the platforms and can change each one without modify the others that are working. The GET
and another HTTP methods are natural to use in this case too.
About the verbosity, is not a problem if this brings clarity about the use of your API.
-
1
Maybe you would like to add a new platform that needs a new information to be passed together with the user reference.
Transpiring the services' details that you are trying to hide just make the facade useless. Body request inputs can be nullified and ignored on the server side. It's a matter of mappings. In other words, how you turn one "post" into a 3rd party post is an implementation detail. You saiduse a single endpoint is a risk
. Not really. It's the safest and more scalable approach in the long run since clients don't to be aware of the "needs" of each platform.Laiv– Laiv2018年03月28日 06:32:41 +00:00Commented Mar 28, 2018 at 6:32 -
On the other hand, seems to me that you are putting too much emphasis on communicating intention through the URI. URI are meant to be meaningless. The simpler are the URI's path, the better. As I commented, simple URIs are less prone to change and as we know, in REST, these identifiers are as good as their faculty to remain unchanged over the time.Laiv– Laiv2018年03月28日 06:40:15 +00:00Commented Mar 28, 2018 at 6:40
-
@Laiv, Ignore some informations is alright in simplest cases, but can be a problem to document how your API works (imagine a Swagger on that) and even understand, because we will create different kinds of information groups that wont play together (like channel and title for a twitter post). Also the versioning only affect one platform and is easier to maintain the code (divide to conquer). I will add this informations on the post.Dherik– Dherik2018年03月28日 10:32:38 +00:00Commented Mar 28, 2018 at 10:32
-
1Versioning in REST could be really counterproductive. Overall when the API doesn't implement Hateoas and the consumers are out of your hand to keep update. For instance, mobile apps. May the forest does not prevent you from seeing the trees, URIs could be something like
/8asdd8ad-3343-ada9dd-asdads99asd
and still make things works. Extract all the meaning possible from the identifier and place it where it should be. in the contract. Whether the contract is reliable or not is a matter of design.Laiv– Laiv2018年03月28日 11:08:23 +00:00Commented Mar 28, 2018 at 11:08
/service/facebook/user/uid123/posts/post345 This feels quite verbose to me
Who is supposed to read this URI over and over. You or the http client? URIs are not required to be "nice" or "readable, just unique. On the other hand, if you are building a gateway, why don't you send the service and user into the POST request body?POST /services/posts/
GET /posts/?q=service:facebook,user:xxxxxxx,from:ts,to:ts