Javascript – REST Api Design Patterns

apiapi-designjavascriptresourcesrest

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).

Best Answer

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.