API design: query string nullable value



  • I have an API endpoint that can return list of Node-s with a nullable parent_id property. If parent_id is null, it is a top-level node. If it is an integer, it points to the id of a different Node.

    User should be able to list nodes for a certain parent, OR just the top-level nodes, OR all nodes, regardless of parent-id.

    So how would I design api endpoint to encompass all that?

    Some options I've considered. Examples are in this order: all nodes, with specific parent, without parent.

    1. /api/nodes?for-parent= | /api/nodes?for-parent=17 | /api/nodes?for-parent=null
      Use string "null" for the NULL placeholder. Converting "null" to null is... ugh.

    2. /api/nodes?for-parent= | /api/nodes?for-parent=17 | /api/nodes?for-parent=false
      Use "false" instead of "null". Maybe safer in terms of client code, but feels even dirtier

    3. /api/nodes? | /api/nodes?for-parent=17 | /api/nodes?for-parent=
      Require clients to carefully avoid adding for-parent if they want all nodes. Cleanest, but the most error prone, it feels.

    4. /api/nodes?for-parent= | /api/nodes?for-parent=17 | /api/nodes?for-parent=&top-level
      Add a separate switch to filter for top-level nodes. Safe but stupid. Creates ambiguous situations.

    Ugh. This is the bike-sheddiest of all bike-shedding dilemmas, but I KNOW I'll waste way too much time waffling around this. Let the WTDWTF decide.



  • @cartman82 I would do something like

    /api/nodes | /api/nodes/17 (or /api/nodes/from/17) | /api/nodes/top



  • @coldandtired said in API design: query string nullable value:

    /api/nodes | /api/nodes/17 (or /api/nodes/from/17) | /api/nodes/top

    I like to use different endpoints for different resources. This is a filter. It doesn't alter the structure of data you will receive. IMO it belongs in the query string or request body.


  • Fake News

    @cartman82 said in API design: query string nullable value:

    1. /api/nodes? | /api/nodes?for-parent=17 | /api/nodes?for-parent=
      Require clients to carefully avoid adding for-parent if they want all nodes. Cleanest, but the most error prone, it feels.

    I see query parameters always as something which is optional by default. If you want everything, just don't specify a filter.

    The only thing I'm in doubt about is the "top-level only" url. It would seem null is more explicit than just an empty string, though either feels like it might work.

    EDIT: I guess null would make the most sense seeing how that's the way you return the parent-id property of top-level nodes.


  • BINNED

    @JBert said in API design: query string nullable value:

    @cartman82 said in API design: query string nullable value:

    1. /api/nodes? | /api/nodes?for-parent=17 | /api/nodes?for-parent=
      Require clients to carefully avoid adding for-parent if they want all nodes. Cleanest, but the most error prone, it feels.

    I see query parameters always as something which is optional by default. If you want everything, just don't specify a filter.

    The only thing I'm in doubt about is the "top-level only" url. It would seem null is more explicit than just an empty string, though either feels like it might work.

    I approve of this message. I'd say make empty string work as a fallback if you really want, but use explicit null in the docs. And I'm only saying this because your backend is probably likely to do that cast anyway (I buttume).



  • @Onyx said in API design: query string nullable value:

    I approve of this message. I'd say make empty string work as a fallback if you really want, but use explicit null in the docs. And I'm only saying this because your backend is probably likely to do that cast anyway (I buttume).

    Yes.

    I initially implemented it as #3, with the empty string converting to null.

    I'll add the explicit "null" option as well.

    It's gonna be more fiddly in the frontend, but I know any other API would just annoy me later every time I see it.



  • @cartman82 That is the way I've always done it.



  • @JBert said in API design: query string nullable value:

    @cartman82 said in API design: query string nullable value:

    1. /api/nodes? | /api/nodes?for-parent=17 | /api/nodes?for-parent=
      Require clients to carefully avoid adding for-parent if they want all nodes. Cleanest, but the most error prone, it feels.

    I see query parameters always as something which is optional by default. If you want everything, just don't specify a filter.

    The only thing I'm in doubt about is the "top-level only" url. It would seem null is more explicit than just an empty string, though either feels like it might work.

    I'd go for explicit 'false' for the no-parent option. It seems a better semantic fit than 'null', which I take to imply 'unknown/don't care' rather than 'not present'. More importantly, if

    /api/nodes?
    means something different from
    /api/nodes?for-parent= or /api/nodes?for-parent=null

    then you are stacking up grief for your users.

    /api/nodes?for-parent=false

    seems much less error-prone to me.



  • @japonicus Just leave it an empty string i.e.

    Then on your end do something like (yes I know it is C#):

    if(!String.IsNullOrEmpty(Request.QueryString["for-parent"])  {
        //Some Logic
    }
    

    Or whatever the equivalent is in Node JS.

    Then if the consumer of the service doesn't supply the parameter, or does supply the param with an empty value. It won't make a difference when you handle the request. Then specify the parameter optional in your docs.


  • SockDev

    @lucas1 I'd advise IsNullOrWhitespace over IsNullOrEmpty, though tbh, 99.9% of the time, it doesn't make a difference.



  • @RaceProUK Yes your are right :-)



  • How about using for-parent=* for all nodes?


  • :belt_onion:

    @cartman82 said in API design: query string nullable value:

    Let the WTDWTF decide.

    you mean, let WTDWTF find 18 more variations that you could use, each more hideous than the last until you give up and implement your 1st option?



  • I would accept any non-integer (empty string, missing parameter, "null", "false", "Belgium", etc.) for top level nodes.

    Pick one to document, but accept any.



  • @japonicus said in API design: query string nullable value:

    /api/nodes?for-parent=false
    seems much less error-prone to me.

    That seems the most ridiculous though. Now you're looking the the node with the parent id of false which is just silly.

    Optional parameter. If you provide it, you better provide it with the correct information. I'd probably also change it to parentId

    Though, why look up a node based on its parent id instead of its own id?



  • @coldandtired said in API design: query string nullable value:

    @cartman82 I would do something like

    /api/nodes | /api/nodes/17 (or /api/nodes/from/17) | /api/nodes/top

    I initially thought this, but the id is the id of the parent which is not what I would expect from an API over this.


  • I survived the hour long Uno hand

    @JazzyJosh if it's REST, it should be maybe a sub-node of the parent: so like:

    /api/nodes for all nodes

    /api/nodes/17/children for the children of node 17

    but I'm not sure how to specify "only the top-level nodes" in this scheme either



  • @Yamikuronue /api/nodes/roots ?



  • @Yamikuronue said in API design: query string nullable value:

    @JazzyJosh if it's REST, it should be maybe a sub-node of the parent: so like:

    /api/nodes for all nodes

    /api/nodes/17/children for the children of node 17

    but I'm not sure how to specify "only the top-level nodes" in this scheme either

    I just logged in to suggest that. If you want top level nodes, just do /api/nodes/roots

    ( you guys are too fast :-) )


  • Fake News

    @Yamikuronue said in API design: query string nullable value:

    @JazzyJosh if it's REST, it should be maybe a sub-node of the parent: so like:

    /api/nodes for all nodes

    /api/nodes/17/children for the children of node 17

    but I'm not sure how to specify "only the top-level nodes" in this scheme either

    Hmmm, you want path parameters? :trolleybus:

    /api/nodes for all top-level nodes
    /api/nodes/13/items/14/items/15/items for all child nodes of node 15
    /api/nodes?id=15 to find just one node and get a 303 See Other response pointing to the URL above
    resolve ../.. against current url /api/nodes/13/items/14/items/15/ to get parent



  • @Yamikuronue, @cartman82

    Generally in systems like Sitecore CMS which uses a node structure for the API the C# API works something like this (this isn't the exact API I can't remember it off the top of my head, think of it more like C# pseudo-code).

    Sitecore has a URL based API that isn't dissimilar to what is described here.

    e.g. The Master Database (Where all content is stored, published or otherwise) is under /sitecore/content/master or something similar. (I normally download a library that abstracts all the gubbings underneath to save me time).

    Item rootItem = Sitecore.Context.DataBase("master").GetItem("/");
    

    The Url version of that would be:

    Item rootItem = Sitecore.Context.GetItemByUrl("/sitecore/content/master/");
    

    However if you wanna get a node by the ID (which is always a GUID)

    You do

    Item item = Sitecore.Context.GetItem(<Guid>);
    

    This isn't too much different than jQuery.

    Where you always have a reference to parent in the jQuery object via parent().

    So I would design the API the otherway around.

    In your response object (assuming it is JSON, yes I know comments aren't supposed to be in JSON) I would have something that looked like this.

    {
        id: '<guid>',
       parentId: '<anotherguid>',
       /*
         other properties.
      */
    }
    

    If I wanted to get the root node to drill down I would have an endpoint that would be like

    /api/nodes/root

    Get the parent id and recurse move down the tree.


  • SockDev

    One of the main things about REST is it's (meant to be) simple to use and understand. To that end, I've yet to see a better solution that the following:

    • /api/nodes for the full tree
    • /api/nodes/17 for the tree rooted at node 17
    • /api/nodes/roots for the roots only

    /me puts on ASP.NET Web API hat

    That would be an easy set of routes to map too, as you define just three routes:

    • /api/nodes
    • /api/nodes/{id:int}
    • /api/nodes/root

    /me takes off ASP.NET Web API hat



  • @RaceProUK That was what I was getting at. Your version is much clearer though.


  • SockDev

    @RaceProUK you idiot! You can do it in two roots!

    • /api/nodes/{id:int?}
    • /api/nodes/root

  • Winner of the 2016 Presidential Election

    @cartman82 said in API design: query string nullable value:

    User should be able to list nodes for a certain parent, OR just the top-level nodes, OR all nodes, regardless of parent-id.

    @cartman82 said in API design: query string nullable value:

    all nodes, with specific parent, without parent.

    These are two different sets.

    IMHO, the @Yamikuronue + @JazzyJosh solution looks best, if you can live with using paths instead of parameters. If you want to stick with parameters you could skip all the fussing about with null, false, etc if you just assign 0 as the parent id for top-level nodes, and not use the parameter if you want the whole tree.



  • What is the range of values for parent_id? If it's a database sequence, a good value for nodes with no parent would be 0. If 0 is a valid node ID, you could use -1 instead. Not including for-parent as a query parameter would get you all nodes.



  • @Dragnslcr I believe everyone is assuming there is one parent.

    If it was multiple parents you would do something like

    • /api/parent1/nodes/root
    • /api/parent1/nodes/{id}
    • /api/parent2/nodes/root
    • /api/parent2/nodes/{id}

    Sitecore does something similar in their C# API

    For Master Database

    Item rootItem = Sitecore.Context.DataBase("master").GetItem("/"); 
    

    For Web Database

     Item rootItem = Sitecore.Context.DataBase("web").GetItem("/"); 
    

    So the equivalent would be

    • /api/master/nodes/root
    • /api/web/nodes/root

    And so forth.

    EDIT: This is the exact syntax you would use in the sitecore api for some smartass said this is the API, it just a close approximation to illustrate a point.



  • @RaceProUK

    @Path("/api/nodes/{id}")
    public Node getChildrenOfNode(@PathParam("id") final int nodeId);
    

    :colbert:



  • @JazzyJosh Can you do that now because I am doing

    [HttpGet, Route("nodes/{id:int}")] 
    

    Sorry @Path is a python decorator.

    EDIT: is it Python or Java?

    EDIT 2: Python doesn't have public or private so I guess Java.


  • Winner of the 2016 Presidential Election

    @JazzyJosh said in API design: query string nullable value:

    @RaceProUK

    @Path("/api/nodes/{id}")
    public Node getChildrenOfNode(@PathParam("id") final int nodeId);
    

    :colbert:

    What does :colbert: have to do with it?!





  • @lucas1 Edit it is Java just saw the final. The Last time I used Java was JDK 1.5.


  • Discourse touched me in a no-no place

    @cartman82 I'd go with having an absent for-parent query parameter as indicating all nodes (subject to any other filters), and having the constrained versions being a comma-separated list of what values to restrict to; the empty string would then be the parentless-nodes, whereas the string 17,38 would be the nodes with parents 17 or 38. You could emphasise this by calling it a for-parents (with the s) query parameter.

    I would not expose a null as a string in there; that feels like exporting the implementation to me, not the real RESTful API. And false would just make Baby Datatype-Modelling Jesus cry.



  • @dkf The false is unnecessary just leave it empty and check for it server side ...



  • @dkf said in API design: query string nullable value:

    And false would just make Baby Datatype-Modelling Jesus cry.

    What about /api/nodes?for-parent=FILE_NOT_FOUND?
    :trolleybus:



  • @japonicus THAT IS THE ANSWER



  • @japonicus said in API design: query string nullable value:

    What about /api/nodes?for-parent=FILE_NOT_FOUND?

    Just make sure it has a frame to securely mediate everything.


  • Discourse touched me in a no-no place



  • @dkf said in API design: query string nullable value:

    @cartman82 I'd go with having an absent for-parent query parameter as indicating all nodes (subject to any other filters), and having the constrained versions being a comma-separated list of what values to restrict to; the empty string would then be the parentless-nodes, whereas the string 17,38 would be the nodes with parents 17 or 38. You could emphasise this by calling it a for-parents (with the s) query parameter.

    Not sure if I like the idea of query string parameters having different meaning when absent and empty. Might just be a personal peeve of mine, but I'd expect /endpoint?q= and /endpoint to have the same meaning. Kinda like having someFunction(x) and someFunction(x, null) do different stuff is a code smell to me.





  • As a complete noob - why not have one parameter for parent ID, and another for roots? Say, ?parent-id=1 and ?root=yes, where having both is error?



  • @cartman82 Here's my opinion on how to structure a URL-based API. Basically, I'd combine parts of #3 and #4 from your options (and steal some ideas from the others ;)). (I like to think that I like safe and clean code.) Just keep in mind that you are exporting an interface, not your implementation.

    1. Empty parameters should be the same as missing parameters. /api/nodes and /api/nodes?for-parent= should return the same set (all nodes).
    2. Toggles are the exception. They can be implemented either as [missing | present] (any assigned value is ignored) or as [(missing / present with falsy value) | present with truthy value], depending on which makes the most sense for your use case. Just be sure to document which one you choose.
    3. Decide whether your API should be constructive or restrictive. Let your user do whichever of special custom filtering or combining they need. Therefore, treat & as either an OR operator so that the returned nodes are all nodes that match any of the parameters or an AND operator so that the returned nodes match all of the parameters. Make sure this is also documented.
    4. {Optional} Allow comma-separated values to be passed to the for-parent parameter to include all the children that have any of the values as a parent_id. If this is implemented, it needs to be explained in the documentation.


  • @japonicus said in API design: query string nullable value:

    /api/nodes?
    means something different from
    /api/nodes?for-parent= or /api/nodes?for-parent=null
    then you are stacking up grief for your users.
    /api/nodes?for-parent=false
    seems much less error-prone to me.

    Yes that was my reservation as well.
    But "false" feels like you're cancelling the filter. And with "null" there's the danger it can auto-convert in some frontend code, the same as with the empty / non-existent value.



  • @PleegWat said in API design: query string nullable value:

    How about using for-parent=* for all nodes?

    But does that mean all the nodes with some parent, or all the nodes, regardless on whether they have parent or not?



  • @darkmatter said in API design: query string nullable value:

    you mean, let WTDWTF find 18 more variations that you could use, each more hideous than the last until you give up and implement your 1st option?

    Pretty much :)

    I just need an external stimuli to prevent me from faffing with this.



  • @ben_lubar said in API design: query string nullable value:

    I would accept any non-integer (empty string, missing parameter, "null", "false", "Belgium", etc.) for top level nodes.
    Pick one to document, but accept any.

    I would have done it like that when I was younger.

    With experience, I'm moving more towards the camp of "define your interface precisely and stick with it". Turns out clients don't appreciate the vagueness of the API, the server has to maintain fiddly coercion code all over the place, the testing is more difficult and there is sadness all around.



  • @JazzyJosh said in API design: query string nullable value:

    Though, why look up a node based on its parent id instead of its own id?

    This is list of nodes, not single node getter.



  • @Yamikuronue said in API design: query string nullable value:

    @JazzyJosh if it's REST, it should be maybe a sub-node of the parent: so like:
    /api/nodes for all nodes
    /api/nodes/17/children for the children of node 17
    but I'm not sure how to specify "only the top-level nodes" in this scheme either

    That could actually make sense.

    Then I could only have the top-level switch, to filter for no parent id...



  • @RaceProUK said in API design: query string nullable value:

    /api/nodes/17 for the tree rooted at node 17

    But how do I then get just the node 17, without the entire subtree?



  • @Dragnslcr said in API design: query string nullable value:

    What is the range of values for parent_id? If it's a database sequence, a good value for nodes with no parent would be 0. If 0 is a valid node ID, you could use -1 instead. Not including for-parent as a query parameter would get you all nodes.

    I considered that. 0 could work. But IMO it leaves equal or more space for client bug as null and false, and isn't any cleaner or more "proper".


Log in to reply
 

Looks like your connection to What the Daily WTF? was lost, please wait while we try to reconnect.