49

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,653
  • 1
  • 16
  • 31

5 Answers5

10

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,485
  • 7
  • 27
  • 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
5

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
  • 61
  • 1
  • 2
5

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
  • 371
  • 1
  • 3
  • 14
4

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
  • 44,913
  • 16
  • 86
  • 129
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
  • 521
  • 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