56

Currently, we are developing an API for our system and there are some resources that may have different kinds of identifiers. For example, there is a resource called orders, which may have an unique order number and also have an unique id. At the moment, we only have URLs for the id, which are these URLs:

GET /api/orders/{id}
PUT /api/orders/{id}
DELETE /api/orders/{id}

But now we need also the possibility to use order numbers, which normally would result into:

GET /api/orders/{orderNumber}
PUT /api/orders/{orderNumber}
DELETE /api/orders/{orderNumber}

Obviously that won't work, since id and orderNumber are both numbers.

I know that there are some similar questions, but they don't help me out, because the answers don't really fit or their approaches are not really restful or comprehensible (for us and for possible developers using the API). Additionally, the questions and answers are partially older than 7 years.

To name a few:

1. Using a query param

One suggests to use a query param, e.g.

GET /api/orders/?orderNumber={orderNumber}

I think, there are a lot of problems. First, this is a filter on the orders collections, so that the result should be a list as well. However, there is only one order for the unique order number which is a little bit confusing. Secondly, we use such a filter to search/filter for a subset of orders. Additionally, a query params is some kind of a second-class parameter, but should be first-class in this case. This is even a problem, if I the object does not exist. Normally a get would return a 404 (not found), but a GET /api/orders/?orderNumber=1234 would be an empty array, if the order 1234 does not exist.

2. Using a prefix

Some public APIs use some kind of a discriminator to distinguish between different types, e.g. like:

GET /api/orders/id_1234
GET /api/orders/ordernumber_367652

This works for their approach, because id_1234 and ordernumber_367652 are their real unique identifiers that are also returned by other resources. However, that would result in a response object like this:

{
  "id": "id_1234",
  "ordernumber": "ordernumber_367652"
  //...
}

This is not very clean, because the type (id or order number) is modelled twice. And apart from the problem of changing all identifiers and response objects, this would be confusing, if you e.g. want to search for all order numbers greater than 67363 (thus, there is also a string/number clash). If the response does not add the type as a prefix, a user have to add this for some request, which would also be very confusing (sometime you have to add this and sometimes not...)

3. Using a verb

This is what e.g. Twitter does: their URL ends with show.json, so you can use it like:

GET /api/orders/show.json?id=1234 
GET /api/orders/show.json?number=367652

I think, this is the most awful solution, since it is not restful. Furthermore, it has some of the problems that I mentioned in the query param approach.

4. Using a subresource

Some people suggest to model this like a subresource, e.g.:

GET /api/orders/1234 
GET /api/orders/id/1234   //optional
GET /api/orders/ordernumber/367652

I like the readability of this approach, but I think the meaning of /api/orders/ordernumber/367652 would be "get (just) the order number 367652" and not the order. Finally, this breaks some best practices like using plurals and only real resources.

So finally, my questions are: Did we missed something? And are there are other approaches, because I think that this is not an unusual problem?

Community
  • 1
  • 1
Indivon
  • 1,784
  • 2
  • 17
  • 32

6 Answers6

12

to me, the most RESTful way of solving your problem is using the approach number 2 with a slight modification.

From a theoretical point of view, you just have valid identification code to identify your order. At this point of the design process, it isn't important whether your identification code is an id or an order number. It's something that uniquely identify your order and that's enough.

The fact that you have an ambiguity between ids and numbers format is an issue belonging to the implementation phase, not the design phase.

So for now, what we have is:

GET /api/orders/{some_identification_code}

and this is very RESTful.

Of course you still have the problem of solving your ambiguity, so we can proceed with the implementation phase. Unfortunately your order identification_code set is made of two distinct entities that share the format. It's trivial it can't work. But now the problem is in the definition of these entity formats.

My suggestion is very simple: ids will be integers, while numbers will be codes such as N1234567. This approach will make your resource representation acceptable:

{
  "id": "1234",
  "ordernumber": "N367652"
  //...
}

Additionally, it is common in many scenarios such as courier shipments.

MaVVamaldo
  • 2,505
  • 7
  • 28
  • 50
  • Thanks! I also think this is the most restful one. from the theoretical POV, you're right. But, I think the design phase should also consider how a param can/must look and how the api user must handle them. Do he/she must add an "N" or not?! Another problem is: "ordernumber" isn't created in our system (an ERP-system creates them) - so we cannot change it and adding some text (here: "N") in our system would clash with the enterprise-wide unique number. We currently think about adding the param-name to the ```{some_identification_code}``` like here: http://stackoverflow.com/a/9743414/4245733 – Indivon Dec 13 '16 at 07:29
  • If you feel uncomfortable to call it "implementation phase" you can call it "secondary design phase". I was just pointing out that pure API design and problems with IDs are separated problems. I make a strong distinction for the sake of conprhension. However the fact you cannot generate your order numbers doesn't imply that you cannot have a remapping at API level. One thing are the business models another thing are API models. – MaVVamaldo Dec 13 '16 at 08:00
  • json spec for number (integer) does not contain double quotes, see: -https://www.json.org/json-en.html -https://json-schema.org/understanding-json-schema/reference/numeric.html#id4 Double quotes are for strings. Although the approach may work, a cleaner solution is to keep a single field as the resource identifier. The secondary field being used as a query parameter filter would be a cleaner (although chattier) solution. GET /orders/{id} Returns a single item GET /orders?ordernumber={ordernumber} Returns a list with all orders with the same ordernumber (even when the field is unique) – Vinicius Scheidegger Mar 28 '22 at 20:59
6

Here is an alternate option that I came up with that I found slightly more palatable.

GET /api/orders/1234
GET /api/orders/1234?idType=id //optional
GET /api/orders/367652?idType=ordernumber

The reason being it keeps the pathing consistent with REST standards, and then in the service if they did pass idType=orderNumber (idType of id is the default) you can pick up on that.

twalbrecht
  • 71
  • 1
  • 2
6

I'm struggling with the same issue and haven't found a perfect solution. I ended up using this format:

GET /api/orders/{orderid}
GET /api/orders/bynumber/{orderNumber}

Not perfect, but it is readable.

Rune
  • 381
  • 1
  • 3
  • 14
6

I'm also struggling with this! In my case, i only really need to be able to GET using the secondary ID, which makes this a little easier.

I am leaning towards using an optional prefix to the ID:

GET /api/orders/{id}
GET /api/orders/id:{id}
GET /api/orders/number:{orderNumber}

or this could be a chance to use an obscure feature of the URI specification, path parameters, which let you attach parameters to particular path elements:

GET /api/orders/{id}
GET /api/orders/{id};id_type=id
GET /api/orders/{orderNumber};id_type=number

The URL using an unqualified ID is the canonical one. There are two options for the behaviour of non-canonical URLs: either return the entity, or redirect to the canonical URL. The latter is more theoretically pure, but it may be inconvenient for users. Or it may be more useful for users, who knows!

Another way to approach this is to model an order number as its own thing:

GET /api/ordernumbers/{orderNumber}

This could return a small object with just the ID, which users could then use to retrieve the entity. Or even just redirect to the order.

If you also want a general search resource, then that can also be used here:

GET /api/orders?number={orderNumber}

In my case, i don't want such a resource (yet), and i could be uncomfortable adding what appears to be a general search resource that only supports one field.

Tom Anderson
  • 46,189
  • 17
  • 92
  • 133
0

So basically, you want to treat all ids and order numbers as unique identifiers for the order records. The thing about unique identifiers is, of course, they have to be unique! But your ids and order numbers are all numeric; do their ranges overlap? If, say, "1234" could be either an id or an order number, then obviously /api/orders/1234 is not going to reference a unique order.

If the ranges are unique, then you just need discriminator logic in the handler code for /api/orders/{id}, that can tell an id from an order number. This could actually work, say if your order numbers have more digits than your ids ever will. But I expect you would have done this already if you could.

If the ranges might overlap, then you must at least force the references to them to have unique ranges. The simplest way would be to add a prefix when referring to an order number, e.g. the prefix "N". So that if the order with id 1234 has order number 367652, it could be retrieved with either of these calls:

/api/orders/1234
/api/orders/N367652

But then, either the database must change to include the "N" prefix in all order numbers (you say this is not possible) or else the handler code would have to strip off the "N" prefix before converting to int. In that case, the "N" prefix should only be used in the API calls - user facing data-entry forms should not expose it! You can't have a "lookup by any identifier" field where users can enter either id or order number (this would have a non-uniqueness problem anyway.) Instead, you must have separate "lookup by id" and "lookup by order number" options. Then, you should be able to have the order number input handler automatically add the "N" prefix before submitting to the API.

Fundamentally, this is a problem with the database design - if this (using values from both fields as "unique identifiers") was a requirement, then the database fields should have been designed with this in mind (i.e. with non-overlapping ranges) - if you can't change the order number format, then the id format should have been different.

Kevin Perry
  • 541
  • 3
  • 6
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 19 '21 at 14:15
0

For me, the solution (1) that you present is the best way, that is:

GET /api/orders/?orderNumber={orderNumber}

Why do I like it the most? Because I can build consistent, intuitive narration around it, without breaking REST style and appealing to some non standard looking approaches. Is it ideal and you can't say any "buts"? No. But it's simple, which is a value in itself, and good enough with addressing issues that you mention.

So what narration am I proposing?

As a base a priori position, let's state that orders have "order number" property, values of which "are unique", but we chose to treat this property, in and of itself, as a meaningless detail from the API semantics point of view (and only there, not in general - bear with me). When considering purely your REST API semantics, it's just interesting characteristic of the values distribution in your data set, which you independently happen to know exists, and won't change in the foreseeable future, but we are not drawing this knowledge directly from the API itself.

From the API point of view, there is one special unique attribute on orders, which your API is both guaranteeing to be unique and is presenting as the key by which you can fetch objects by its identity. All other fields:

  • all have the property that they are not special unique attributes,
  • all may or may not be declared as unique in any given version of your API,
  • may, in principle, come and go during the lifetime of the development of your system.

In other words, orderNumber is just an example of a field which is currently unique and currently you are saying this property won't change. There may be more such fields now or in the future, too.

Now, to address your arguments against this approach.

First, this is a filter on the orders collections, so that the result should be a list as well. However, there is only one order for the unique order number which is a little bit confusing.

Think of it that way - we can always get a single item list using some filtering criteria having nothing to do with order number.

The only special case here is when we filter by orderNumber and, at the same time, we "want or need to remember about it's uniqueness", for any reason.

In such a case, given that we "know" it's unique, we just treat responses with more than one item on the list the same way we would treat any other unexpected responses, e.g. invalid JSON in the response payload, invalid schema in the response etc. I expand on that in the next point.

Secondly, we use such a filter to search/filter for a subset of orders.

Excellent. Exactly what we want - we look for a subset of orders, and as extra insight, we know we should expect that those subsets will be of size 0 or 1, if we use filtering by orderNumber. Any bigger ones we treat as if we got gibberish from the server.

And what that treatment is, exactly, depends on the app, but as a first approximation, we treat such situation as an equivalent of a response with a totally messed up payload.

Only if we have some actual insight into how we could use the information that this is specifically this particular bug (too many items on a list, and not, say not parsable JSON in the response), then we add some special logic for this.

Is your app currently just crashing on invalid JSON in the API response? Crash it on multiple items returned, too! Is it doing something more clever? Do the same here. Can you get a value from, say, logging that you got this particular issue? Implement dedicated handling code.

Additionally, a query params is some kind of a second-class parameter, but should be first-class in this case.

What I'm proposing is to pay with not treating orderNumber as a first class citizen from the standpoint of the API semantics (as opposed to from other points of view, e.g. user facing UI, where it must keep its status) for the benefit of simplicity of reasoning about it.

This is even a problem, if I the object does not exist. Normally a get would return a 404 (not found), but a GET /api/orders/?orderNumber=1234 would be an empty array, if the order 1234 does not exist.

By now this problem morphs into totally expected behavior - we achieve the state of enlightenment.

zifot
  • 2,688
  • 20
  • 21