Skip to content

Instantly share code, notes, and snippets.

@jordanbyron
Created June 23, 2011 14:48
Show Gist options
  • Save jordanbyron/1042668 to your computer and use it in GitHub Desktop.
Save jordanbyron/1042668 to your computer and use it in GitHub Desktop.
Discussion and design ideas for University Web's User API

le API design

A consumer needs to lookup a single user by their github name (jordanbyron). That consumer doesn't know the user's ID. There is a possibility to also search by email and twitter.

"/users.json?github=jordanbyron"
=> [{github: "jordanbyron", id: 1}]

When there are no results an empty array is returned

"/users.json?github=noexist"
=> []

When there are partial matches, multiple results can be displayed

"/users.json?github=jo"
=> [{github: "jordanbyron", id: 1}, {github: "joeuser", id: 1}]

QUESTIONS

  1. Is there a way to keep a RESTful interface but also retrieve just one record per request (Without knowing the user's ID)
  2. Should the filtering of records only return exact matches, or partial matches as well? Or have the ability to do one or the other?

V1 API from comments

"/users.json?search=jo"
=> [{github: "jordanbyron", id: 1}, {email: "userjoe", id: 1}, {twitter: "iamjoelman", id: 1}]

If there are no fields which match the search params

"/users.json?search=zzzzzzzz"
=> []

When github, twitter, or email params are passed only one result is returned

"/users.json?github=jordanbyron"
=> {github: "jordanbyron", id: 1}

When one of the above params are used and there are no matches, nil is returned

"/users.json?github=noexists"
=> nil

V2 API & wrapper from comments

API

"/users.json?search=jo"
=> [{github: "jordanbyron", id: 1}, {email: "userjoe", id: 1}, {twitter: "iamjoelman", id: 1}]

If there are no fields which match the search params

"/users.json?search=zzzzzzzz"
=> []

When github, twitter, or email params are passed the result is still in an array

"/users.json?github=jordanbyron"
=> [{github: "jordanbyron", id: 1}]

When one of the above params are used and there are no matches, an empty array is returned

"/users.json?github=noexists"
=> []

Wrapper

module UniversityWeb::User
  def self.find_by_github(github_account_name)
    result = service("/users.json?github=#{github_account_name}")  # Makes a request to the server's API

    result.first                                                   # result == [ { github: "jordanbyron", id: 1 } ]
  end
end

UniversityWeb::User.find_by_github("jordanbyron")
=>  { github: "jordanbyron", id: 1 }
@jordanbyron
Copy link
Author

@phiggins if setting that one option not only changes the way data is found but also the result type then I think we need a different endpoint entirely.

But I really would like to avoid something like /users/search or /users/find if at all possible. Just not sure how to do that :-/

@semmons99
Copy link

Pete makes a good point. To take it a bit further, what about only allowing the following options github, twitter, email and search. The first three would only return a hash or nil, the fourth would return an array (having searched all fields) which could potential be empty.

@jordanbyron
Copy link
Author

@semmons99 I kinda like that idea. Feels better to me than the idea of having a limit=1 or extra special param change the result set.

@jordanbyron
Copy link
Author

@semmons99 & @phiggins I've updated the gist to include that new API design we discussed. Let me know if it looks like what you guys had in mind and seems reasonable.

@semmons99
Copy link

@jordanbyron & @phiggins: The new API looks good to me.

@semmons99
Copy link

@jordanbyron: I would update the first example to this:

"/users.json?search=jo"
=> [{github: "jordanbyron", id: 1}, {email: "joeuser", id: 1}, {twitter: "joel", id: 1}]

To make sure it's understood search works over all fields.

@jordanbyron
Copy link
Author

@semmons99 done. I also tweaked the results to show that it will match any position in the string, not just the first few characters.

@phiggins
Copy link

I like it. Thanks for the opportunity to use my nitpicking powers for good!

@jordanbyron
Copy link
Author

@phiggins no problem. Thanks for the feedback!

@samnang
Copy link

samnang commented Jun 24, 2011

I'm not sure having options(github, twitter, email) to to return hash or nil is still keeping Restful style because we should keep "users" resource to return a collection, but having one item in it. So whenever we build a ruby api wrapper, the response from "users" resource doesn't parse differently between those two requests.

@ericgj
Copy link

ericgj commented Jun 24, 2011

@samnang good point. Personally I don't see the need for returning a single item vs an array here, why not always return an array? The consumer knows what it needs - either one or many. If it needs just one, #first it. Easier to deal with and remember the api if it's always an array.

@jordanbyron
Copy link
Author

@samnang & @ericgj clearly I am torn here. I'd like to stay as close to the RESTful style as possible, but also make an API that is nice to use. Sure calling first on an array isn't that hard, but is that an interface that is pleasant to work with? I'm not sure. With the changes we made, it is very clear that when requesting records for a given email, github, or twitter name you are asking for one record, and therefore should expect a single record.

So which is the best way? I'm still not sure. Working on both sides of the problem (API design and API consumption) I don't know if I can be 100% satisfied. The last possible thing I can think of is creating a nice client library that wraps the API and handles returning one or nil records when looking up a user via the three fields (email, github, or email). So the server will still return an array of users, but the library will return one. For example:

module UniversityWeb::User
  def self.find_by_github(github_account_name)
    result = service("/users.json?github=#{github_account_name}")  # Makes a request to the server's API

    result.first                                                   # result == [ { github: "jordanbyron", id: 1 } ]
  end
end

UniversityWeb::User.find_by_github("jordanbyron")
=>  { github: "jordanbyron", id: 1 }

I think that solves all of the issues which were brought up but still has a nice end user experience. Thoughts?

@semmons99
Copy link

I'm not familiar with rails enough to know off hand, but does Person.find(:first) return an array or a single Person object? I'd assume Person.find(:all) returns an array.

@samnang
Copy link

samnang commented Jun 24, 2011

@jordanbyron, I like the last solution that you have mentioned here. Response should be consistence with an array, and a wrapper api gets the response and return only first item whenever users call with find_by_*.

@jordanbyron
Copy link
Author

@semmons99 in rails <= 2 Person.find(:first) does return a single person object (or nil) and Person.find(:all) returns an array.

@samnang cool. I was getting a bit hung up on making sure the service endpoint's API also provided a nice end user interface. But I've come to realize that is just too difficult and creating a wrapper library will get me the best of both worlds. Thanks for your feedback!

@semmons99
Copy link

I do wonder if this really should be two separate RESTful endpoints. One really is attempting to return a specific user and the other is doing a board based search across users. If I liken it to a Hash, in the first case we're doing my_hash[:shane] and in the second doing my_hash.select{|(k,v)| k =~ /shane/}.

@jordanbyron
Copy link
Author

@semmons99 that would be ideal, but in your hash example you know the key for the record you are looking up. In our case, we don't know the record id, so the standard RESTful convention of /users/1 doesn't work when all we have is the github name. Now we could do a little trick and have routes like /users/github/jordanbyron that return a single record, but that again diverges from the standard /resources/id structure. Plus I don't like the way /users/email/jordanbyron@gmail.com looks. In fact, I'm not even sure that would work.

@semmons99
Copy link

@jordanbyron that's true. What about /user?github=jordanbyron for single lookup and /users_search?q=jordan? Or do you really want to just keep /user? for both paths? In the end it's okay to always return an Array, but it seems a little odd that we change the result type with the wrapper, versus having a wrapper that looks more like this:

module UniversityWeb::User
  def self.find(params = {})
    query_params = params.map{|(k,v)| "#{k.to_s}=#{URI.encode(v.to_s)}.join("&")
    result = service("/users.json?#{query_params}")
    result
  end
end

@jordanbyron
Copy link
Author

@semmons99 first off, thanks for showing me (inadvertently) how to do the fancy github syntax highlighting :)

I'm no so hung up on the wrapper returning a different result type, especially since we are creating special helper methods that start with find_by. As an end user I would absolutely expect that to return one object or nil, but maybe that's because I do a ton of rails work.

@semmons99
Copy link

@jordanbyron

As an end user I would absolutely expect that to return one object or nil, but maybe that's because I do a ton of rails work.

That's where I'm at too. As an end user I'd expect looking for one person would be an object or nil whether I use the wrapper method or the REST api.

Isn't the fancy github syntax highlighting awesome? No more having to do extra indenting from what I copy out of my text editor, and I get syntax highlighting. I believe they're using RedCarpet for markdown support. We should pull it into university-web.

@jordanbyron
Copy link
Author

whether I use the wrapper method or the REST api

I guess that is where our opinions differ. I would rather have a fully RESTful API where /users will always return an array of users, and the only way to get one user object is through /users/:id, or by indexing into the array from /users.

I believe they're using RedCarpet for markdown support. We should pull it into university-web.

Hum, I'll have to take a look at that. It should be painless to replace rdiscount. We just have to see how it produces the fancy syntax highlighting stuff.

@samnang
Copy link

samnang commented Jun 25, 2011

I believe they're using RedCarpet for markdown support. We should pull it into university-web.

@semmons99, first of all thank you for sharing this.

It's a good feature to add into university-web, it helps the reader easy to read. And I hope later if we have something like code review in university-web, then it will be awesome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment