3

I refer to this fantastic answer to an earlier question I had.

My question is, say you have an endpoint GET /tasks and that endpoint accepts query params to get certain groupings of data like:

  • GET /tasks?createdDate=01-01-2022&completedDate=30-01-2022
  • GET /tasks?isOpen=true&priority=high

etc.

As a consumer of this API I can use these query params to then derive statistics like:

  • How many high priority tasks are currently open?
  • For the month of January, what was the mean time to complete a task?

etc.

Now the problem is, if I want derive statistics like 'mean time to complete a task, across all time' I'm going to need to pull in every single item just to make that calculation.

I figure that instead the backend could be calculating and storing these statistics on a separate table, and it can update that table as resources change, or cache queries as they occur.

The question is - is it appropriate to use the same endpoint and the Accept header to request these statistical representations?

eg:

GET /tasks?createdDate=01-01-2022&completedDate=30-01-2022
Accept: application/vnd.metric+meanTimeToCompleteTask+json

GET /tasks?isOpen=true&priority=high`
Accept: application/vnd.metric+count+json

Or should a separate endpoint be used?

eg:

GET /metrics?resourceType=task&metricRequired=count&isOpen=true&priority=high

Note: for these metric representations PUT/POST/PATCH/DELETE would not be allowed, they're a GET only operation.

What I like about the the content negotiation approach is that as a consumer of the API, you don't need to familiarise yourself with another API endpoint - your queries are going to match the query you would make if were calculating the values yourself.

Bounty Edit: I want to make it clear why I think using an Accept header against the same endpoint would be a good idea:

  • Necessarily, the same set of query parameters can be used to define the list of data that determines the metric, as is used to fetch said of data itself.
  • If you were to use two separate endpoints, they might well happen to have the same set of query parameters, but that is not a necessary coincidence. It's conceivable that they would fall out of sync.
3
  • Since you did take the trouble to put a bounty on this question, perhaps you can clarify specifically what you mean by "appropriate?" Software construction is always an exercise in tradeoffs. What does "appropriate" mean to you? Most likely to win a beauty contest? Most likely to be accepted by your peers? Commented Feb 28, 2022 at 17:31
  • And please don't respond with a tautology like "most RESTful." Be specific about your goals: what are you optimizing for? Commented Feb 28, 2022 at 17:34
  • Yeah, I appreciate the question @RobertHarvey - I guess 'Be accepted by my pairs', or 'Make to sense to an experienced coming in and say reading the API documentation'.
    – dwjohnston
    Commented Mar 1, 2022 at 4:56

3 Answers 3

3
+250

Groundwork first: REST doesn't have "endpoints" (see Fielding, 2018), it has resources. The current HTTP standard offers this definition:

The target of an HTTP request is called a resource. HTTP does not limit the nature of a resource; it merely defines an interface that might be used to interact with resources.

The resource targeted by the request is that resource identified by the entire target URI -- general purpose components do not distinguish between path segments and the query part.

/foo?a
/foo?b

These are two different resource identifiers, and therefore general purpose clients must assume they are two different resources. (The fact that your server happens to route both of those target URI to the same request handler(s) is an implementation detail).

Therefore, when you talk about:

GET /tasks?createdDate=01-01-2022&completedDate=30-01-2022
GET /tasks?isOpen=true&priority=high

These are already two different resources (as far as the outside world can tell), so you don't need content negotiation to support the different representations, any more than you need to negotiate content when you are talking about /home.html and /home.jpeg


GET /tasks?isOpen=true&priority=high
GET /metrics?resourceType=task&metricRequired=count&isOpen=true&priority=high

Both of these spellings are fine; you could choose either, both, or something else entirely. The machines don't care, so you can follow local spelling conventions, your own tastes, or choose spellings that best support the needs of the human beings you care about. It's all good.


Now, there are cases where it makes sense for a single resource (our generalized abstraction for named information) might have multiple current representations. We might want /test/123 to have a text/plain representation available for clients that won't understand the primary text/html representation. Or perhaps /patch/123 has both application/json-patch+json and application/merge-patch+json.

Note the distinction: we're not using the Accept header to identify the resource we want (that's done by the target URI). Instead, we're trying to identify the representation of that resource which best matches the client's capabilities.

Even when that makes sense, we might still prefer designs where those distinct representations are also available via more specific identifiers (think /home, /home.html, and /home.txt), and requests to the negotiated resource either redirect to the specialized resources or directly embed their representations.

Lots of possible trade offs.

It may be interesting to review Fielding 2006, a discussion of the concerns of language negotiation on the web, and the resource designs in place at his companies.

4
  • A well-designed REST API would invoke /foo/a and /foo/b to represent individual resources, not /foo?a or /foo?b. So your reasoning about /tasks?someQualifier=someValue representing different resources depending on someValue seems a bit odd; it's still about task resources, regardless of the qualifiers after the question mark. Commented Feb 28, 2022 at 16:41
  • Changing the qualifiers described in the query part ("after the question mark") changes the resource identifier, which changes the resource. I suggest reviewing the current standards for constructing responses from caches (datatracker.ietf.org/doc/html/…). Note that the first criteria is that the target URI (which includes the path and the query) match. Commented Feb 28, 2022 at 16:55
  • Is caching likely to be a factor for /tasks?someQualifier=someValue? Presumably, you're looking at a collection, not an individual resource. That is, after all, why it's named tasks and not task. Commented Feb 28, 2022 at 16:59
  • The HTTP application, in the "transfer of documents over a network" domain, does not distinguish collection resources from individual (item) resources. They are all just "resources". You absolutely should not be assuming that identifier spellings are used to communicate caching constraints - that information belongs in headers. Commented Feb 28, 2022 at 17:13
0

A list of tasks and a metric calculated over that list are two different resources and that is a very strong indication that they should be served from different URIs/paths.

I would even go a step further and argue that "number of tasks matching criterion" and "mean time to complete" are different resources and should have different URIs/paths.

That would lead to requests like

GET /metrics/tasks/counte?isOpen=true&priority=high

or

GET /metrics/tasks/mttr?createdDate=01-01-2022&completedDate=30-01-2022
(mttr = mean time to resolve)

This allows the backend to have different strategies for calculating and/or caching the various metrics and doesn't constrain you to put all metrics in the same reply format.

The fact that everything under /metrics is a read-only resource is not an issue at all. It is even easier to understand than that for /tasks some media formats are read/write and some are read-only.

1
  • I think you're stretching the REST metaphor beyond what it is capable of. Nevertheless, your URL schemes are consistent with the spirit of the OP's design, although they are a bit long-winded. Commented Feb 28, 2022 at 17:35
0

TL/DR - Probably not. Recommend the following (or something like it) as a starting point:

GET /task-metrics

In my experience great APIs do two things:

  1. Follow expected conventions for efficient learning, reasoning, and use
  2. Offer a conceptual/logical model that makes sense to consumers

If task metrics is "a thing" for consumers then it makes sense to represent them as first class citizens in your API. Often, different audiences see things differently. For task metrics it may be that "doers" view these as purely derived/secondary whereas "managers" view these as first class elements.

If your API is to serve both audiences then it should make sense to both. It's usually better to increase the number of first class objects in your API than to require one or more audiences to adjust their world view. This allows people to pay attention to the parts they care about and (more likely) be able to ignore the parts they don't.

One might argue this is an API-centric application of the Single Responsibility Principle (SRP).

Regardless of what you decide, good luck!

Your Answer

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

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