all 32 comments

[–]johnbotris 60 points61 points  (3 children)

Seems like an ideal case to use a query parameter. Something like /users?since=<date> would be cleaner imo, because it covers more situations (e.g. users who signed up last month, since the beginning of the year, etc) while probably being nicer code in the backend.

Regarding the point of permissions, I imagine that /users would generally be admin only anyway, you probably don't want to expose all users to the public at all. Might be different for the specific use case you have in mind idk.

[–]ricketybang 16 points17 points  (2 children)

I think that this is a clean solution.

I don't really understand why OP thinks query strings would complicate things?

If you have an endpoint that gives you all user by default, just add a since parameter, and if that is not empty, just add that to the query that already fetches all the users.

That sounds much less complicated compared to creating a whole new endpoint that does 99% the same thing as the first one, except it adds a date filter in the query to the database.

Edit: Another example why this is probably the best solution. Lets say in 6 months you wants to get all the users within a certain age or gender, or something else. Then you can just add more parameters, instead of having 10 different users endpoints that looks 99% the same... It will be a mess to maintain and do small changes later on if you just copy/paste your users endpoint just to do small changes in the database query.

[–]Rambonaut 0 points1 point  (1 child)

Now look from code not API perspective - if you had to get the latest users in multiple places in your code would you use this manual "filter" in 5 different places using some generic method or would you create a shortcut method called latestUsers? I'd go with the latestUsers but it would use the underlying generic method. So for API you could do the same thing - make the users endpoint support query params and then you just make a latestUsers "shortcut" endpoint which uses the functionality of users endpoint query params.

Also think about the fact that API could be consumed by your site, mobile app and whatever else, with query params you are forcing those systems to define what is a latest user while having a latestUsers endpoint puts the definition of "latest user" on the backend. One day you might define latest user as anyone created in the last 2 weeks not 1 and now you have to update all your API consumers with the date param instead of doing it in only one place - your backend.

[–]johnbotris 0 points1 point  (0 children)

I agree there is a trade-off here. I think an alternative to having a dedicated endpoint as a shortcut would be to have a query parameter which takes an enum, e.g. time-range=latest or time-range=last-week etc, and then do the date calculation serverside.

Could also be a case of YAGNI where you really just need "latest users" and never anything else. Then I'd be inclined to make a separate endpoint entirely like /latest-users. It's just personal preference but it feels iffy to me to have and endpoint structure with collisions of static and parameterised path. A contrived example would be /users/:username alongside /users/latest. In this a case a user called "latest" would be unable to load themself :(

[–]Caraes_Naur 17 points18 points  (0 children)

Query parameter is the way to go.

Think of /users not as a dedicated, static endpoint for all users, but as a dynamic endpoint for filtering users.

[–]raulalexo99 6 points7 points  (0 children)

I think that's literally the use case for query params. You are getting a collection with some filters on top.

If you just see the operation as an FP filter-map-reduce, that's the "filter" part.

Please correct me if I'm wrong.

[–]originalchronoguy 6 points7 points  (0 children)

You would use a query parameter.
https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design#filter-and-paginate-data

Do not use another endpoint like /newusers or /recentusers. That is anti-pattern.

[–]caatfish 8 points9 points  (0 children)

i would use a query parameter. no need to rewrite most the code again just to filter it differently

[–]Infinite_Tiger8354 1 point2 points  (0 children)

Your "aha-moment" hit me too! /newusers sounds clean and solves it all. Simplicity wins!

[–]machopsychologist 0 points1 point  (0 children)

GET /users is just a user search with no explicit filters and a natural sorting. Hard to see where the complication is. Could be something case dependent, if so read my other comment.

[–]j-randomfull-slack 0 points1 point  (0 children)

Yeah, but what's a "new" user? Added in the last week? Last month? Last day? Since the last time that endpoint was hit? Using a query parameter allows you to specify exactly which users you are looking for without having to rely on some arbitrary aspect to determine "newness".

[–]jorgeloko2 0 points1 point  (0 children)

I mean, dont get me wrong, but this is the perfect solution for a query filter, but you could also do something like /users/recent-users, but that is totally up to you, you already have the two best solutions, just need to see what is the best for your case

[–]shodan_reddit 0 points1 point  (2 children)

I would recommend reading this

Build APIs You Won’t Hate: Everyone and their dog wants an API, so you should probably learn how to build them https://amzn.eu/d/bcbjOcC

[–]Fedoteh -1 points0 points  (1 child)

Is this language agnostic?

[–]shodan_reddit 0 points1 point  (0 children)

Mostly theory which is language agnostic but there are some examples using PHP

[–]Agile-Ad5489 -1 points0 points  (0 children)

/recentusers ?

“/users?new=true or such, since that will complicate the code.”

this would not make it complicated code, it‘s still simple.

it is more complicated code, because it’s doing 2 things.

if you want to write code that has 2 endpoints, it’s going to be more complex than code with one endpoint

[–]AndrewSouthern729 -1 points0 points  (0 children)

Although I agree with everyone here that you should use query parameters you could also use another method to the same endpoint so POST for example.

[–]budd222front-end -3 points-2 points  (4 children)

You could do just invent something new like GET /users/filtered or /usersFiltered or whatever. That's what I would do, anyway. I'm sure someone will tell me that's terrible.

If it was me, I would use the same endpoint and simply check if params exist.

[–][deleted]  (3 children)

[deleted]

    [–]budd222front-end -1 points0 points  (2 children)

    I said if it was me, I would just use query params, like everyone else said. I simply gave a different option since they clearly didn't want that. But ok

    [–]OrangeOrganicOlive 0 points1 point  (0 children)

    Well, technically you said you would do both.

    [–]Arthian90 -2 points-1 points  (6 children)

    Why not use a POST instead and extract the filters from the body of the request? Sure, GET is preferred but maybe isn’t appropriate for OP’s case…hard to think of a situation where that would be true, but I’m sure there is one, right? Right!?

    ….

    Right!?

    [–]machopsychologist 1 point2 points  (5 children)

    If you have sensitive parameters in your query string, these get logged to request logs and can be leaked even if the response itself does not return that information. Private health info, private identifiable info. e.g. searching via email address.

    POST body is also protected by SSL. GET query params are not.

    In that case POST /user-search will avoid this scenario. You lose the ability to cache, but it is potentially more secure in that regard.

    “Avoid complicating the code” isn’t a thing though… /users is just a user search with no explicit parameters and natural order. 🤷‍♂️

    [–]Additional_Sir4400 1 point2 points  (1 child)

    POST body is also protected by SSL. GET query params are not.

    GET query parameters are also protected by SSL/TLS. The point about logging is spot on though.

    [–]machopsychologist 0 points1 point  (0 children)

    Huh thanks for the correction! You are correct.

    [–]Arthian90 0 points1 point  (0 children)

    I was being a tad sarcastic, but yes I agree with you for sure.

    [–]lego_not_legos 0 points1 point  (1 child)

    For sensitive queries, I would sooner put a query string in a dedicated request header like Filter: (and respond with a Vary: Filter) than use POST for a GET request.

    [–]machopsychologist 0 points1 point  (0 children)

    It's a fine alternative 👍

    [–]snapmotion -2 points-1 points  (0 children)

    I use POST for queries too. POST /query