46

I am having a bit of difficulty designing a url for a rest service that can handle requests for customers based on pagination as one type of operation or requesting greater than or less than operators as another type of operation. For example:

Pagination:

GET /customers/0/100

This will get 100 customers for page 0.

Greater/Less Than:

I also need a URL design to get customers that have an id greater than n (e.g. lets say 716). How would you incorporate "greater than" or "less than" in a url. I have to bear in mind that characters ">" and "<" are illegal in urls. I think this url design looks odd:

GET /customers/greaterthan/716
GET /customers/lessthan/716

I can't use a range as that would conflict with the pagination pattern specified above and is not a nice solution in any case e.g.:

GET /customers/716/999999999999
GET /customers/0/716

I'm sure that I'm missing something obvious - does anyone have a better solution?

Henke
  • 4,445
  • 3
  • 31
  • 44
Vidar
  • 6,548
  • 22
  • 66
  • 96
  • how do you plan to implement this in real time scenario--just for curiosity – UVM Jan 06 '11 at 11:50
  • it's being done using the Restlet framework in Java. Essentially, I'm querying a database for records. – Vidar Jan 06 '11 at 11:54

11 Answers11

45

Pagination, greaterthan and lessthan, sound like query parameter to me, since you are queries your resource with these parameters. So you should do something like:

/customers?page=1, or
/customers?page=1&gt=716, or
/customers?page=1&gt=716&lt=819

You can even limit size of page:

/customers?page=1&gt=716&lt=819&maxpagesize=100

where gt stands for greater than (same as in xml-escaping) and lt stands for less than.

halfer
  • 19,824
  • 17
  • 99
  • 186
Tarlog
  • 10,024
  • 2
  • 43
  • 67
  • sorry I think I've confused everyone here - I don't mean do pagination and more than, less than operations all at the same time. Pagination is a separate function and the less/greater than is another type of function. – Vidar Jan 06 '11 at 13:53
  • 2
    It doesn't matter. Pagination and less/greater are unrelated issues. You should identify your resource. In your case the resource is customers. Both pagination and less/greater are query on the resource, but it's still the same resource. Therefore you should not try to insert your functionality in the url (path-param), but instead you should use query params. At the end your call will reach the same resource class (in java), than you can add your query params to the SQL query or something like this. – Tarlog Jan 06 '11 at 14:18
  • Even if you put the params in the query string, you are still referring to a different resource. That's ok though. Two different resources can still render portions of the same underlying dataset. Simple rule: If it's a different URL and it doesn't redirect then it is a different resource. – Darrel Miller Jan 06 '11 at 14:37
  • also, you should make sure your query string params are always in the same order (assuming they are independent), otherwise /resource?a=1&b=2 is a different resource from /resource?b=2&a=1 and a cached entry for one cannot be returned for the other – Nicholas Shanks Nov 16 '12 at 14:51
  • 1
    It seems Tarlog has provided a good solution here however others might find this a useful resource for API design and practice... http://apigee.com/about/api-best-practices/all/webcast – Opentuned Nov 22 '12 at 15:33
  • Why gt/lt instead of symbols? (<>) – themihai Aug 14 '16 at 12:20
  • <> are special characters. – Tarlog Aug 14 '16 at 19:34
28

If you have multiple parameters and need to apply some conditions for each params, I recommend you to pass a JSON object to params.

Consider you want to do a condition for id and the page:

/customers?id={"lt": 100, "gt": 30}&page={"start": 1, "size": 10}

It says that I want customers that have Id(s) less than 100 and greater than 30 in the page 1 and page number of 10.

So now simply if you want to apply another condition for other parameters, you can do it by:

/customers?id={"lt": 100, "gt": 30}&children={"lt": 5, "gt": 2}&page={"start": 1, "size": 10}

and this query means customers with Id(s) less than 100 and greater than 30, children less than 5 and greater than 2 in the page number 1 with page size of 10.

I highly recommend you to read this document about designing RESTful API: http://blog.luisrei.com/articles/rest.html

nabinca
  • 2,002
  • 1
  • 18
  • 29
Afshin Mehrabani
  • 33,262
  • 29
  • 136
  • 201
14

Here is how I have been doing it.

Let's say you have the field age which is a number.

This is what the urls would look like
Equals: /filter/age=5
Greater Than: /filter/age[gt]=5
Greater Than Equals: /filter/age[gte]=5
Less Than: /filter/age[lt]=5
Less Than Equals: /filter/age[lte]=5
Not Equals: /filter/age[ne]=5

Then when I pass these arguments to the back-end I have a script that just parses the key out and converts it to the correct filter based on the age[INSERT_OPERATOR_HERE]

Hackbyrd
  • 436
  • 5
  • 12
10

@Julio Faerman:

Well, the problem starts when you get multiple parameters. Imagine the query string for "Customers older than 18 and younger than 60 with more than 2 children".

You can define whatever query parameters you like, such as:

/customers?min-age=19&max-age=59&min-children=3

in my example min and max are integers and are inclusive. You can change that if you prefer. Remember, anything in the URI connotes part of the resource identifier. My personal view is that things after the ? equate to clauses in the WHERE part of an SQL query (plus ORDER BY and LIMIT, not shown here):

SELECT * FROM customers WHERE age>=19 AND age<=59 AND children>=3

Edit:
Instead of min- and max- prefixes, you could allow >, < (and maybe !) as the final character of the parameter name, so instead of min-age you have a parameter called age>, which, when combined with a value in the query string, ends up looking like age>=19 :-)
Obviously you can only use this trick when the comparison has an equals sign in it.

Nicholas Shanks
  • 10,623
  • 4
  • 56
  • 80
  • 1
    Seems most reliable way to stick with rest! If they use json in query - then way better to go for mongo query style and POST with x-method-override = get. Otherwise everything arrived after "?" - referred as filtering options. – Valentin Rusk Jun 21 '15 at 12:08
  • @ValentinRusk I didn't understand that. Can you re-post it in your first language so I can translate myself? Ты русский? – Nicholas Shanks Jun 21 '15 at 16:18
  • Лучший выбор для REST !!! Если использовать JSON в GET, тогда лучше идти по пути стиля Mongo Query + header: x-method-override. В первом случае, все что приходит после "?" - считается фильтрами. – Valentin Rusk Jun 22 '15 at 11:19
  • 1
    @ValentinRusk Okay, not a question, just a compliment :-) Спасибо! – Nicholas Shanks Jun 25 '15 at 16:55
7

REST is an architectural style that should not be considered as specific to HTTP. The pattern of the URIs are not what makes an architecture RESTful.

With that said, you probably would want to make your URI so that these queries come as query parameters at the end of the string, e.g.

/customers?min=0&max=76
pc1oad1etter
  • 8,549
  • 10
  • 49
  • 64
4

Some REST-based standarts offer adequate approaches to this problem. For example, https://www.hl7.org/fhir/stu3/search.html

Your problem can be solved like that: GET /customers?id=ge500&id=lt1000

Also, there's OData which is far more interoperable that any industry-level standard. And it proposes this style: GET /customers?$filter=id ge 500 and id lt 1000

astef
  • 8,575
  • 4
  • 56
  • 95
3

Let's say you are going to fetch your server logs and suppose that your data looks like this:

{
    "protocol": "http",
    "host": "example.domain.com",
    "path": "/apis/classified/server/logs",
    "method": "GET",
    "ip": "::ffff:127.0.0.1",
    "time": 1483066346338,
    "usingTime": 12,
    "status": 200,
    "headers": {
        "user-agent": "Mozilla/5.0 Chrome"
    }
}

And you want to query like this way, where

  • protocol equals 'http'.
  • host equals 'example.domain.com', or not equals 'example.domain.me'.
  • path equals '/apis/classified/server/logs', or likes /*classified\/server*/.
  • method equals 'DELETE' or not equals 'GET' or in ['POST', 'PUT', 'DELETE'].
  • ip equals '127.0.0.1', or not equals '127.0.0.1'.
  • usingTime is greater than 3500, or is greater than or equal to 1500 and less than or equal to 3500.
  • status equals 404, or not equals 200, or is greater than or equal to 400 and is less than 500.
  • headers.user-agent likes /*chrome*/i.

Here the routes go like this:

  • /apis/classified/server/logs?path=/apis/classfied
  • /apis/classified/server/logs?path.regex=*classfied*
  • /apis/classified/server/logs?method.ne=GET
  • /apis/classified/server/logs?method=POST&method=PATCH&method=PUT
  • /apis/classified/server/logs?usingTime.gte=1500&usingTime.lte=2500
  • /apis/classified/server/logs?headers.user-agent.regex=*chrome*

If you are using express.js as the server, your req.query will be structured like:

  • {"path": "/apis/classfied"}
  • {"path": {"regex": "*classfied*"}}
  • {"method": "DELETE"}
  • {"method": ["GET","POST","PUT"]}
  • {"usingTime": {"gte": "1500","lte": "2500"}} (lte: is less than or equal to)
  • {"status": {"ne": "200"}}} (ne: is Not Equal to)
  • {"path": {"regex": "*classfied*"}}
  • {"headers": {"user-agent": {"regex": "*chrome*", "flag": "i"}}}

And you are gonna use a lot of if-else to make up your mongoose query method, or SQL string maybe.

Fisher
  • 359
  • 3
  • 5
0

I would implement it as a range, and if either side is open, just do not fill it.

GET /customers/16-
GET /customers/-716

When requesting all customers, do not add all, just leave it empty

GET /customers

Or, when you need - signs in your number/code, use this:

GET /customers/16/
GET /customers//716
GET /customers/16/716

You can escape the forward slashes if they are part of the number.

Rob Audenaerde
  • 19,195
  • 10
  • 76
  • 121
  • 2
    I stumbled across this today. What an elegant solution. Unfortunately, it only works for IDs, not for queries, so `/customers/16-25` is fine, because we *know* that the '-' indicates a range, but if it were query field, it wouldn't work: `/customers?name=123-127` is that customers where the value of name is from 123 to 127, or customers who have the name equals the string "123-127"? No way to tell. – deitch Nov 10 '14 at 18:31
  • 1
    I like this answer. Also check tiny range module for nodejs ( https://www.npmjs.com/package/tiny-range ) , you can use this notation as well : ~1,2,3,5~10,20~ – Shrey Sep 14 '16 at 07:01
0

I suggest to use querystring.

And I suggest one of the following templates:

?amount=~gt~100&age=~gt~16

?amount=gt100&age=gt16

?amount=gt 100&age=gt 16

?amount=greater_than(1)&age=less_than(69)
hoi ja
  • 239
  • 5
  • 8
-1

WARNING: Read disclaimer below

I dont know if this quetion is related to c#. But if you are using linq for your queries on the server side, I have another suggestion:

GET /customers?filter=CustomerNo>=16 AND CustomerNo<=716

Then, on the server side:

using System.Linq.Dynamic;

var filteredCustomers = myCustomerTable.Where(filter);

This should be the most flexible solution of all, I think.


WARNING added january 19, 2017 It has just come to my attention that a code injection exploit exists for Dynamic Linq. The example above opens up the possibility that the client can start code execution on the server.

  • It looks like an `GET /database?query=SELECT * FROM customers WHERE CustomerNo>=16 AND CustomerNo<=716` endpoint. – Yami Odymel Jan 23 '19 at 19:10
-3

I'd take

 GET /customers/greaterthan=16;lessthan=716/

as the order is not important. You can even paginate on top:

 GET /customers/greaterthan=16;lessthan=716/page/10

Alternatively (with an appropriate Request router):

 GET /customers/16-716/page/10

And without "filter":

 GET /customers/all/page/10
b_erb
  • 20,932
  • 8
  • 55
  • 64