Styleguide for creating / modifying endpoints

This style guide is mostly inspired on REST API Design Rulebook by Mark Massé.

Readability

This is something often overlooked when designing API’s, most of us think of readability as something that applies to our code or comments only, but our resources should be as readable or more as the code we are writing. More so since we are planning to make a part of our API accessible.

Here are some “Rules” regarding readability that we should consider when looking at our URI’s

  • Always Hyphen <-> vs under_scoring or camelCasing

Which is harder to read (try putting your mouse over it)? read_this vs read-this

  • Don't include potential media types in the URI

There is no need to have a URI that looks like </resource/pdf> and then another one </resource/docx>. That is something that should be relied to the server who should read the content-type header to correctly address client needs.

  • Request methods should identify CRUD actions not URI’s

URI should not specify what the method is doing to the resource, or collection of resources. e.g </resource/deleteResource> or </user/getUser/{id}> are not good, DELETE </resource> and DELETE </user/{id}> are what’s expected.

  • Pagination or any filtering should be done in the query component of the URI.

Pagination or any filtering should be delegated to the query component of the URI, as is a natural fit for it. So for a new endpoint don’t do GET </farms/{pageStart}/{pageEnd}/{customFilter}> instead do GET </farms?currentPage=0&pageSize=10&randomCustomFilter=23>

  • Resource access should be hierarchical and descriptive

When you are creating an endpoint, think about the relation the entities that will be accessed hold. So in our system for example, a farm has many fields, it would be weird to have your expected URI be

GET /field/farm/{farmId}

or even weirder

GET /farm/field/{farmId}/{fieldId}

the URI structure should follow the resource hierarchy so if a farm has many fields, read for reference

GET /farm/{farmId}/field

Functionality

There are a few things to take a look at when looking at how a request is being handled. Like, what is in the header, environment considerations, testing and more.

Generic Middleware

For everything that needs to be done independently of the request, or in many similar requests like: verify, transform, decode, encode or even third party usage, you need to define a middleware for it and use it on each route that’s needed.

For the sake of explaining what a middleware is, and how we are expecting it to be developed, lets create a middleware that logs whatever the request body is in the case of a POST method. We will have this middleware in a hypothetical file called, postLogger.js

module.exports = (req, res, next) => { console.log('Body of POST: '); console.log(req.body); next(); }

As you can see, this will simply log the body of a request and then call next (which will call the next function in line)

And you would use this middleware in every post request of your app’s routers. like so:

const express = require('express'); const router = express.Router(); const loggerMiddleware = require('./middleware/postLogger'); router.get(....) router.delete(...) router.put(...) router.post('/', loggerMiddleware, someController.postFuncionality ); module.exports = router;

As you can see is really as simple as appending the function to one of your routers parameters. Do remember, order matters, if you were to change the 2nd to the 3rd parameter, then the order in which they will be called will change as well.

There might be a case, when you want to have a middleware that applies to your whole app, let's say we modify our logger and we filter our logging depending on the environment and the HTTP method and rename it logger.

module.exports = (req, res, next) => { if(req.method === 'POST' && process.env.NODE_ENV === 'DEV'){ console.log('Body of POST: '); console.log(req.body); } else { console.log(req.method); console.log(req.originalUrl); } next(); } // logger.js

Now this method applies to every route on the system, to apply this middleware to every route on the app, append it to the server app listener. Like this:

Now this middleware will be called before hitting any route on the server, remember, order here matters as well. If you want the middleware to apply to every route, you need the utilization (app.use) to be before every route (or before any routes you need to apply it for).

Request structure and handling

Headers

Client headers should generally be used to communicate authorization tokens and content-types, any other use must be carefully addressed to the team to determine if it fits the project needs. Other uses that might come up, Cache control or CORS headers (for development).

We are deprecating user_id, farm_id or any other id being sent in as headers, all user related information should be encoded in the token payload.

URL

The URL structure will vary depending on the type of request we are expecting.

In the case of GET, endpoints if we want to return an array of any collection type like farm, GET /farm should return an array of farms, if we want to get a specific item of that collection you use its as a parameter of the request id so GET /farm/123 should return Farm with id 123 or 404 if no farm was found.

In case we have an endpoint that returns a big collection which is too large to request directly and you need to filter the size of the request, you should send pagination parameters on the query of it, so if you want to get the first 10 farms that the DB returns you can do GET /farm?pageSize=10&page=1 which should return the first 10 farms. Pagination is a candidate for a Generic middleware. other custom filters should also be included on the query side of the URI.

In the case of a DELETE, you only want to specify which specific item of a particular collection you want to delete DELETE /farm/123 should delete farm 123 or return 404 if no farm was found.

In the case of a PUT you want to specify which item of a particular collection you want to modify, so PUT /farm/123 should attempt to modify farm 123.

POSTs are the most flexible of the requests, since you could either be creating entities or just performing actions (as sending an email or a notification) their URLs can vary a lot.

Body

We are not allowing any body on GET and DELETE requests. this requests should follow resource/{id} structure as mentioned in the rules above.

The body of the request is to be validated, for POST and PUT requests, so users that send the wrong data should receive notice of it with a 400 response.

After creating a resource the user should be noticed with a 201.

 

Response handling

We need to be able to respond to users appropriately according to the action they performed and what the result of that action was.

For All Requests:

For all requests, if any outside user tried to access an endpoint that is behind the authorization wall we should return a 401 Not authorized code

For all requests, if a user is trying to access a resource he is not allowed to access (a worker trying to create a field for example) we should return a 403 Forbidden Code

For POST and PUT requests of a specific request does not pass validation, we should return a 400 bad request code

For GET requests:

After handling the request if the resource could be obtained we should be able to respond with the data of the resource and a 200 Success Code.

If we tried to obtain a resource but could not get any data, then we need to return a 404 Not Found Code.

If we tried to obtain a resource and failed for some reason that’s unknown to us, then the user needs to be sent a 500 Internal Server Error Code

For DELETE requests:

After handling the request if the resource could be deleted we should be able to respond with the a message confirming it and a 200 Success Code.

If we tried to delete a resource but could not get any data, then we need to return a 404 Not Found Code.

If we tried to delete a resource and failed for some reason that’s unknown to us, then the user needs to be sent a 500 Internal Server Error Code

For PUT requests:

After handling the request if the resource could was updated we should be able to respond with the recently updated resource in our response body and a 200 Success Code.

If we tried to update the resource but could not get its instance, then we need to return a 404 Not Found Code.

If we tried to delete a resource and failed for some reason that’s unknown to us, then the user needs to be sent a 500 Internal Server Error Code

For POST requests:

After handling the request if we were creating a new resource we should be able to respond with the new resource in our response body and a 201 Success Code.

After handling the request if we were performing any other action and we did so successfully we should respond in with a 200 Success Code .

If we tried to handle the request and failed for some reason that’s unknown to us, then the user needs to be sent a 500 Internal Server Error Code