4

I'm developing a REST Api using Node.js & Express.js. Recently I've started looking towards best practices for REST Api design but it's bit confusing so please bear with me.

For Example:

Lets say I have the following API Collections:

  1. Users
  2. Tasks (every task must belong to a User)

And Have these resources:

  • GET /api/users returns all Users
  • GET /api/users/:id returns a specific User
  • POST /api/users will create a single User
  • POST /api/users/:id will update a specific User
  • POST /api/tasks will create a new Task but will assign it to the loggedIn User.
  • POST /api/tasks/:id will update a specific Task
  • GET /api/tasks will get all Tasks
  • GET /api/users/:id/tasks will get all Tasks for a specific User
  • GET /api/users/:id/tasks/:id will get a specific Task for a Specific User

Now each "Resource" above is redirected towards a "Controller" that actually performs actions.

For example:

POST ('/api/users', myController)

let myController = (req,res) => {
    Create new User
    Save to Database
    Call Private Send an Email to Admin function etc
 }

Questions:

Lets say I have to perform some queries on a resource such as filter should I:

OPTION 1:

Create a new resource like so /api/users/:id/tasks/filter and within my Controller and perform the filtering actions on the database within the controller

OPTION 2:

Use Query Strings like so /api/users/:id/tasks?filter=title::someValue&priority::someOtherValue. According my knowledge doing this will NOT create a new resource but will simply call the /api/users/:id/tasks resource with the above Query Strings.

If I follow OPTION 2 thats means I will only have one Controller that manages the /api/users/:id/tasks actions so to handle any Queries for this Resource should I do the following:

GET ('/api/users/:id/tasks', myController)

let myController = (req,res) => {

    if(req.filter)
    {handle Query here and get data from database and return to user}

    else if(some other query)
    {handle query here}

    else if(some other query)
    {handle query here}

    else {
      if there are NO queries then get get all Tasks for a specific User.
    }

 }

Is the above implementation/concept correct? To me it seems messy and confusing. So what am I doing wrong here? What's the correct way of handling queries? Should a single Controller for a resource handle everything for that particular resource?

I'm having trouble thinking in "REST" there is so much information I'm not sure what right and whats wrong. So my basics are a bit weak so any guidance towards that would be very helpful (any articles or online resources).

1
  • Can you explain what the filter is used for. It seems like you are using the filter for things that the URL should do. For example instead of of /api/users/:id/tasks?title:someValue why not do /api/users/:id/tasks/:title. Complex query strings are a headache, but as Laiv points out this isn't really anything to do with REST and more just to do with how you implement a system that takes an arbitrary amount of parameters and performs an action. Keep the query string as simple as possible and use URLs for everything else Commented Dec 5, 2017 at 10:10

2 Answers 2

1

Disclaimer

Despite the question has been scoped as REST, the truth is that it has nothing to do with REST.

Long story short, how to map the request parameters to an specific query language is implementation details. So, for now, lets leave REST aside.


Is the above implementation/concept correct?

For us to say whether is correct or not, we would need to be familiar with the project and its requirements. So far, what we can say is: the solution is not flexible.

Flexible in the sense of scaling in the long run. Every new filter would take you (at least) to modify the controller, the service layer and maybe the data access layer too. From the management standpoint, is hard to buy so much work for so little feature. In other words, the maintenance looks expensive.

Nevertheless, it depends on how often you add new parameters to the query.

So what am I doing wrong here?

You are missing two possible abstractions.

  1. The one that maps a dynamic set of request parameters to an specific model of the API.
  2. The one that maps the previous model to your specific query language.

Note: Some frameworks makes #1 optional.

Right now the mapping is hardcoded as a if/else block. Too rigid for scaling well.

Instead, It could be similar to:

myController(req,res){

   var query = new QueryParametersMapper(req);

   var page = new PageParametersMapper(req);

   var sort = new SortParametersMapper(req);

   var dataSet = myRepository.find(query, page, sort);

  // etc...
}

What's the correct way of handling queries?

Making the mapping more dynamic and reusable.

At this point, my first advice would be look for libraries compatible with your framework or programming language.

My second advice would be, pick the one that supports Convention over configuration.

Even if It doesn't prevent you from making changes, at least (I'm sure) the costs of the changes will be lesser than they are now.*


* If we consider the code given as example in the question.

0

Here are my opinions as a RESTful API developer. Some of these are nothing to do with REST but just good HTTP practice.

  • Take Option #2 and use query strings. This will be better for intermediaries like the browser's cache.
  • Instead of ?filter=title::someValue&priority::someOtherValue I would recommend ?title=someValue&priority=someOtherValue
  • To use ?filter=title::someValue&priority::someOtherValue the way you had intended you would need to URL-encode the ampersand anyway.
  • I strongly recommend not putting /api/ in your URLs. There is no difference between an API endpoint targeted at humans vs. one targeted at machines other than the representation, which is independent of the URL. Sharing a URL allows the same links to be followed by humans or machines and for each to get the appropriate representation for them. If /users/:id returns a HTML page and /api/user/:id returns JSON, you're "doing it wrong", because both of those URLs represent the same thing (technically, it's not wrong—multiple URLs are fully entitled to identify the same resource—but it could be better).
  • Likewise, I would not use /api/users/:id/tasks/:id for individual tasks, and have a /tasks collection and /tasks/:id under that, with links from /users/:id/tasks going directly to the latter. /tasks and everything under it would be access-controlled just as /users is.

You also say:

According my knowledge doing this will NOT create a new resource

It absolutely will! Every unique URL is a different resource. Doesn't matter if it returns the same content as another URL, or redirects to another URL, or has a <link rel=canonical> to some other URL, or is a subset of another URL. A URL identifies an abstract concept of a 'resource', some of which can be represented as a byte-stream, and some cannot.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.