简体   繁体   中英

Rest API design with multiple unique ids

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, eg

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, eg 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 eg 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 eg 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, eg:

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?

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.

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.

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.

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.

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, eg 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 (ie with non-overlapping ranges) - if you can't change the order number format, then the id format should have been different.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM