3

I work in the SAP industry where web-based UIs and RESTful APIs are a rather new thing, meaning some teams (like ours) don't have much experience in designing such systems. Recently there's been a dicussion that revolved about whether our API should be an abstract representation of the resource or client-specific by pre-filtering a subset of the resource. Let me try to elaborate using the specific use case that was the center of the discussion:

Say we have a business object work-task that has a property called status. That status may have several values, say active and closed. Now, a web-based UI is being designed that allows users to edit work-tasks, but only if they have the status active.

Before I explain the two opinions on how to design the API, let me state some important prerequisites:

  • The validation whether a work-task can be edited (the status check) is always done on the server during UPDATE.
  • There will likely come the requirement to develop different clients that e.g. require work-task regardless of status
  • There are no security concerns regarding the status property being visible to the user

These are the two opinions on how to approach this (I obviously have a clear preference, but I'm going to try to recite both to the best of my abilities):

  1. Design an abstract API and allow the client to filter for status='active' in the query. The goal is to make the API easily reusable. That includes that the client developer needs to know part of the use case (e.g. the kind of status he needs to filter). It also means that if the business logic ever changes (e.g. another editable status is being introduced, say, partly-active), the client would need to be changed (to include that value into his query filter).
  2. Design a client-specific API and filter server-side instead of client-side. To achieve this, three approaches were being suggested: a) Include a technical field into the resource representation that specifies the client, say forClient. The client would then filter that property for e.g. forClient='App1'. b) Include a 'technical association', between a parent resource and the actual resource which would lead to the query look something like this: work-package/forApp1/work-tasks. Work-package would be an object that has a 1:n relationship to work-task. c) Develop a new completely new API per client.

Of course I have consulted the API specs (we use OData V2.0, which is specified here), but the specs never talk explicitely about that point. One could argue that 2) is never being talked about and (1) is always being assumed when looking at e.g. the URI or resource conventions.

I myself use JSON:API personally, which seems to be a little more restrictive and could be interpreted to more specifically demand (1).

I've also consulted guidelines on how to implement REST, like here and here. I'd also interpret these as to go for (1). But in the end, I might be too biased and trying to read between the lines.

I'd very much welcome any input on this matter. :)

Bonus Question: In case (or in cases where) you would go for (1), would that change when there wouldn't be only one property to filter, but say 3-5 filters would be required to get the subset required by the client?

3 Answers 3

3

Should an artist paint based on commissions, or should they paint what they want and then sell it?
Should a contractor build prefab houses, or should they build custom blueprints?
Should a doctor be visited by their patients, or should they visit their patients?

All of these questions are the same. There is no specific answer to them. It depends on what your particular goal is. Maybe you have a clear opinion on all of these. Maybe you don't.

For example, one of the clients I'm working for has had bad experiences with workloads involving customization for different clients (and the conflicts arising from it), and has now adopted a very stark "we build it the way we do, the consumer just has to accept that" attitude. Their codebase never considers a request from a specific client, at best they address issues as being for "clients", i.e. everyone at the same time. E.g. "clients are asking for the ability to do X" as opposed to "Client A is asking for the ability to do X". Either everyone gets it or no one does. It keeps things really simply but it does turn some customers off.

On the other hand, a previous client I worked at specifically prided themselves on their ability to adapt to a customer's specific needs and would cater to their every whim, at the cost of the development workload this entails.

Neither of them are wrong, they're just in a different business. This is why we often speak about "products" versus "solutions" in terms of development strategy.

Products are built based on their own needs, and are finished without any particular customer in mind. Solutions are tailor-made to a specific customer, and that includes cases where the groundwork is a reusable product that is then tweaked to the customer's specifications.

So, are you building a product or a solution?


As a baseline, your software has two endpoints:

  • Fetching tasks
  • Editing a task

The two are unrelated to each other. It would be perfectly viable (from your end) to let the customer figure out which task they can edit and which they can't.

That is of course not a great user experience. But it does achieve the first (absolutely barebones) milestone: you have a working backend.

Next, you get to decide how much time and effort you spend on making your backend nicer to work with, and in which way. This can come in many different shapes and forms:

  • Returning the status when fetching the tasks
  • Allowing pre-emptive filtering of tasks based on status
  • Returning a isEditable flag in your returned task data
  • ...

There is no universal solution here. It all depends on what you need.

  • For example, if no one would ever use the filtering except to figure out if something can be edited, that's a really inefficient way of going about it, e.g. when showing a list of all tasks, some of which can be edited.
  • Then again, if users spend a lot of time fetching the tasks, a filtered endpoint is going to add much usage value to your API.
  • Then again, if we're never talking about more than a handful of tasks (10 or less) being returned, then the list is too short to really benefit from the complex filtering and you're better off just returning extra data in the task objects.

As you can see, there are plenty of situations in which one approach can be better than another, but there is no universally superior solution.

There will likely come the requirement to develop different clients that e.g. require work-task regardless of status

We cycle back to the original question. Is your backend a product or a solution?

If you intend to have different frontends (one per client) which all use the same backend, then either:

  • Client-specific rules would need to live on the frontend (ease of development, but not secure
  • OR Your backend needs to expand to cater to every single client at the same time (secure, but takes more effort)

If you want a high degree of customization in your backend as well, then it may be better to simply split off the backends (one per client) so that you don't have to deal with conflicts between different clients' rules. But it takes more deployment overhead to do so.

Or, you can instead merge it all into the same codebase to save on deployment overhead cost, at the cost of development complexity.

Most things in software development are a trade-off. Depending on what your company is willing to invest in or what they already have, certain decisions may be more desirable than others, but that doesn't always translate to other companies/people.

  1. Design an abstract API and allow the client to filter for status='active' in the query. The goal is to make the API easily reusable. That includes that the client developer needs to know part of the use case (e.g. the kind of status he needs to filter). It also means that if the business logic ever changes (e.g. another editable status is being introduced, say, partly-active), the client would need to be changed (to include that value into his query filter).

Your main issue here can be avoided by not using an "active" filter to solve your problem, but rather to rely on the concept of a task being "editable" (a calculated property on the task, based on your validation rule). The backend clearly defines the editability of an object, so it can rely on its own knowledge to decide whether a task is editable or not.

This can take the shape of having an "editable" filter on the request, where you only return the editable tasks (and without the filter, you return all tasks).

Or, alternatively, you can add an isEditable field to the tasks you return, so that it is immediately known if a specific task is editable or not.

Personally, I'd implement both. It gives you the best of both worlds. The filter means you transport less data when the consumer doesn't care about the ineditable tasks, and the isEditable flag means that regardless of the filter being activated or not, the frontend is always acutely aware whether a task can be edited or not.

That way, the frontend just asks the backend for the editable objects, and the backend gets to decide what the criteria for being editable are. If you change your criteria for editability tomorrow, you only need to redeploy the backend, and the frontend doesn't need to be changed.

a) Include a technical field into the resource representation that specifies the client, say forClient. The client would then filter that property for e.g. forClient='App1'.
b) Include a 'technical association', between a parent resource and the actual resource which would lead to the query look something like this: work-package/forApp1/work-tasks. Work-package would be an object that has a 1:n relationship to work-task.

Be very careful about these kinds of fields (whether in the URL or the request body is irrelevant for my point).

This allows any caller to arbitrarily change the client they're representing. While it might take some guesswork as to which clients exist, if this is the summation of your security, it is easily circumventable and end users will find ways around their own business rules by pretending to be another client.

It would be much more secure to define the client based on the user account. This way, you know that your business rules (and the choice of client thereof) is at least as secure as your authorization platform itself is.

c) Develop a new completely new API per client.

That's definitely an option, but we get back into the "how much effort are you willing to spend, and what's the gain" line of reasoning.

10
  • Thanks for your long and in-depth answer, but I think there is a major misunderstanding here: By "client" I mean consuming app. There is no such thing as a customer. We are an inhouse development team and we develop everything, from the frontend (in the form of a webapp) to the API to the business logic in the backend. "client-specific" means "per consuming app". The user does not filter or need to know which 'work tasks' are editable, because I, as the frontend dev, apply the filter.
    – Leo
    Apr 20, 2021 at 14:14
  • @Leo: If you are the frontend dev, then you (or, more specifically, your frontend apps) are the clients of the backend. For the purposes of backend development, it doesn't matter whether the backend consumers belong to the same company or not. The concerns should be separated regardless. It also doesn't matter whether the end user of the frontend app explicitly knows that they're fetching a list of only the editable tasks, or whether the frontend app does so silently (again, for the purposes of developing the backend specifically).
    – Flater
    Apr 20, 2021 at 14:16
  • Can you elaborate on how (1) is in conflict with validation of an UPDATE request happening on the backend side? The client does not define editability, it just asks the backend to edit a resource. The backend itself would then check e.g. for the correct status, among other things (e.g. authorization).
    – Leo
    Apr 20, 2021 at 14:20
  • Also, we're really only talking basic GET-Requests here. You might be assuming more complexity to the use case I'm describing, but it's really only "should we filter for status='active' client-side or backend-side" (by using one of the three approaches I've described in (2)). The maximum amount of complexity would be to include a few more filters into the query. REST seems to explicitely encourage using filters (among other things like pagination) in queries to reduce the fetched data to the required subset.
    – Leo
    Apr 20, 2021 at 14:37
  • @Leo "Can you elaborate on how (1) is in conflict" Having an abstract API which is client-agnostic is conceptually orthogonal to having an update endpoint which requires knowledge of the client (since it needs to apply the specific client's validation rule, in this case "editable only if active"). As mentioned in the answer, that doesn't mean you can't have a generically reusable GET endpoint, but I'd urge for consistency whereby the UPDATE and GET endpoints are either both client-agnostic or neither are.
    – Flater
    Apr 20, 2021 at 15:09
0

Should our RESTful API be abstract or client-specific?

Should your website be abstract or client-specific?


One common variation you'll see within websites is that we'll have localized representations of the pages. English speakers can look at English representations, French speakers can look at French representations, and so on.

It's normal to have many different resources with representations derived from common data.

That said, different resources with representations derived from common data are (when viewed from the client) different resources. Clients are working from their local copies of the representations of your resources; how is the client to know which resources have changed? HTTP has some standards for how caches should respond to messages, but we don't currently have a general purpose mechanism for indicating all of the resources that have changed.

In some cases, having a single resource with many different negotiable representations is an option, which is nice in that invalidating the resource will invalidate all of the representations. But content negotiation doesn't necessarily make things simpler.

As usual "it depends"; there are a number of trade offs you can make, but the best trades aren't the same in all contexts.

1
  • Yeah, I get that it depends on the use case. Obviously, two clients could require such different representations of a resource that it would make sense to e.g. have two separate APIs. But I feel like that is not the case in the scenario I described.
    – Leo
    Apr 20, 2021 at 12:48
0

These are the two opinions on how to approach this

I would go for option 1 but I wouldn't use a query string. Instead use a URI like

GET work-tasks/active

that will return active work tasks. You could also have URIs like

GET work-tasks/closed

and when you add a new status for a work task you can simply add a new URI.

This does mean that the client will have to understand it is getting active work tasks. If I understand your question you were hoping for a solution where the client gets work tasks but doesn't know the work tasks are filtered, the server filters based on what the client identifies itself as.

If you really want to do that (and I'm not sure why you would), the correct way to do this is using the User-Agent header in the request

So the client requests

GET work-tasks

but identifies itself in the header as User-Agent: App1 and thus the server can customize the work-tasks resource for that specific client (eg only return active work tasks)

But I would not do this unless you have a very strong reason why the the client should not be aware it is getting active work tasks when it asks for all work tasks. This is not how REST is designed to work (the server shouldn't care what the client is)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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