Why Use Node to Build Beautiful APIs?

Posted on the 30 October 2018 by Aben @appscrip

Beautiful APIs in Node

 What is an API?

The definition says Application Programming Interface(API), but what does it mean? It could mean one of the few things depending on the context:

  • Endpoints of a service service-oriented architecture (SOA)
  • Function signature
  • Class attribute and methods

Keeping it simple, API is a form of a contract between two or more entities (objects, classes, concerns, etc.).

The main goal as a Node engineer is to build beautiful API. The rest of your code can be ugly but the parts which are public, need to be conventional, extendable, simple to use, understandable & consistent.

Beautiful Endpoints in Node

 What is an API endpoint?

The endpoint of an API is simply a unique URL that represents an object or collection of objects. The endpoint is what you’ll point your HTTP client at, to interact with data resources.

Most likely, you are not using core Node HTTP module directly, but a framework like Express/Hapi. If not, then strongly consider using a framework. It will come with freebies like parsing and route organization. Let’s take examples using the Express framework.

Here’s our API server communicating with the /accounts resource listed with an HTTP method:

  • GET /accounts: Get a list of accounts
  • POST /accounts: Create a new account
  • GET /accounts/{ID}: Get one account by ID({} means it’s a variable)
  • PUT /accounts/{ID}: Partial update one account by ID
  • DELETE /accounts/{ID}: Remove one account by ID

You can notice immediately that we need to pass the resource (account) ID in the URL for the last three endpoints. By doing so we achieve the goals of having a clear distinction between resource collection and individual resource.

This, in turn, helps to prevent mistakes from the client side. For example, it’s easier to mistake DELETE /accounts with ID in the body of the request, & you can easily be fired if this ever happens in production as it causes the deletion of all accounts from the database.

Additional benefits can be derived from caching by URL. If you use Varnish, it caches responses and by having /accounts/{ID} you will achieve better caching results. Express framework here will just ignore request body(payload) for requests like DELETE so the only way to get that ID is via a URL.

Express is very elegant in defining the endpoints. For the ID which is called a URL parameter, there’s a req.params object which will be populated with the properties and values as long as you define the URL parameter (or several) in the URL pattern, for example, with :id.

app.get('/accounts', (req, res, next) => {
  // Query DB for accounts
  res.send(accounts)
})

app.put('/accounts/:id', (req, res, next) => {
  const accountId = req.params.id
  // Query DB to update the account by ID
  res.send('ok')
})

Now, talking about PUT is for a complete update. However, a lot of API use PUT as a partial update. Did Foer example, if you update with {a: 1} an object {b: 2}, the result is {a: 1, b: 2} when the update is partial and {a: 1} when it’s a complete replacement.

A more proper way is to use PATCH endpoints for partial updates instead of PUT. However, PATCH is lacking in implementation. Maybe that’s the reason why a lot of developers pick PUT as a partial update instead of PATCH.

How do we get the actual JSON?

There’s body-parser which can give us a Node-JavaScript object out of a string.

const bodyParser = require('body-parser')
// ...
app.use(bodyParser.json())
app.post('/accounts', (req, res, next) => {
  const data = req.body
  // Validate data
  // Query DB to create an account
  res.send(account._id)
})

app.put('/accounts/:id', (req, res, next) => {
  const accountId = req.params.id
  const data = req.body
  // Validate data
  // Query DB to update the account by ID
  res.send('ok')
})

It’s essential that the incoming/outgoing data should be validated. Modules like joi and express-validator to help you sanitize the data elegantly.

In the code snippet above, you might have noticed that the ID of a newly created account is passed. This is the best practice because clients will need to know how to reference the new resource.

Another best practice is to send proper HTTP status codes such as 200, 401, 500, etc, use cases of which are listed below.

  • 20x: All is good
  • 30x: Redirects
  • 40x: Client errors
  • 50x: Server errors

By providing a valid error message you can help developers on the client side because they can know if the request failure is their fault (40x) or server fault (500).

In Express, status codes are chained before the send(). For example, for POST /accounts/ we are sending 201 created along with the ID:

res.status(201).send(account._id)

The response for PUT and DELETE doesn’t have to contain the ID because we know that the client knows the ID(used in the URL). A good idea is to send back some okay message. A simple one would be like {“msg”: “ok”} or an advanced one like –

{ 
  "status": "success",
  "affectedCount": 3,
  "affectedIDs": [
   1,
   2, 
   3
  ]
}

 What about query strings?

They can be used for additional information such as a search query, filters, API keys, options, etc. To pass additional information, usage of query string data for GET is recommended.

For example, this is how you can implement pagination. The variable page is the page number and the variable limit is how many items are needed for a page.

app.get('/accounts', (req, res, next) => {
  const {query, page, limit} = req.query
  // Query DB for accounts 
  res.status(200).send(accounts)
})

Pure & Impure Functions in Node

Node and JavaScript are very (not completely) functional. We can create objects with functions. A general rule is that by keeping functions pure you can avoid future problems.

 What is a pure function?

A pure function doesn’t depend on and doesn’t modify the states of variables out of its scope. Concretely, that means a pure function always returns the same result given the same parameters. Its execution doesn’t depend on the state of the system. Here is a pure function:

let randomNumber = null
const generateRandomNumber = (limit) => {
  let number = null  
  number = Math.round(Math.random()*limit)
  return number
}
randomNumber = generateRandomNumber(7)
console.log(randomNumber)

 What is an impure function?

An impure function is a function that mutates variables/state/data outside of its lexical scope, thus deeming it “impure” for this reason. Below example is a very impure function because it’s changing randomNumber outside of its scope. Accessing limit out of scope is an issue too because this introduces additional interdependency (tight coupling):

let randomNumber = null
let limit = 7
const generateRandomNumber = () => {
  randomNumber = Math.floor(Math.random()*limit)
}
generateRandomNumber()
console.log(randomNumber)

The second snippet will work alright but only up to a point in the future as long as you can remember about the side effects limit and randomNumber.

 What is a side effect?

side effect is any application state change that is observable outside the called function other than its return value. Side effects include modifying any external variable or object property (e.g., a global variable, or a variable in the parent function scope chain)

There are a few things specific to Node and function only.

They exist because Node is asynchronous. For async code, we need a way to schedule some future code execution. We need to be able to pass a callback. The best approach is to pass it as the last argument. If you have a variable number of argument, still keep the callback as last. You can use arguments to implement it.

For example, we can re-write our previous function from synchronous execution to asynchronous by using callback as the last argument pattern. I intentionally left randomNumber = but it will be undefined since now the value will be in the callback at some point later.

let randomNumber = null
const generateRandomNumber = (limit, callback) => {
  let number = null  
  // Now we are using super slow but super random process, hence it's async
  slowButGoodRandomGenerator(limit, (number) => {
    callback(number)
  })
  // number is null but will be defined later in callback 
}

randomNumber = generateRandomNumber(7, (number)=>{
  console.log(number)
})
// Guess what, randomNumber is undefined, but number in the callback will be defined later

The next pattern which is closely related to async code is error handling. Each time we set up a callback, it will be handled by an event loop at some future moment.

When the callback code is executed we don’t have a reference to the original code anymore, only to the variable in scope. Thus, we cannot use try-catch and we cannot throw errors like we do in Java and other synchronous languages.

For this reason, to propagate an error from a nested code (function, module, call, etc.), we can just pass it as an argument to the callback along with the data (number). You can use return to terminate the further execution of the code once an error is found. While using null as an error value when no errors are present (inherited or custom).

const generateRandomNumber = (limit, callback) => {
  if (!limit) return callback(new Error('Limit not provided'))
  slowButGoodRandomGenerator(limit, (error, number) => {
    if (number > limit) {
      callback(new Error('Oooops, something went wrong. Number is higher than the limit. Check slow function.'), null)
    } else {    
      if (error) return callback(error, number)
      return callback(null, number)
    }
  })
}

generateRandomNumber(7, (error, number) => {
  if (error) {
    console.error(error)
  } else {
    console.log(number)
  }
})

Once you have your async pure function with error handling, move it to a module. You have three options:

  • File: The easiest way is to create a file and import it with require()
  • Module: You can create a folder with index.js and move it to node_modules.
  • Set private: true to avoid publishing.
  • npm Module: Take your module a step further by publishing it on npm registry

In either case, you would use CommonJS/Node syntax for modules since the ES6 import is nowhere near TC39 or Node Foundation roadmap. The rule of thumb when creating a module is what you export is what you import.

In our case, it’s function:

module.exports = (limit, callback) => {
  //...
}

And in the main file, you import with require. Just don’t use capital case or underscores for file names.

const generateRandomNumber = require('./generate-random-number.js')
generateRandomNumber(7, (error, number) => {
  if (error) {
    console.error(error)
  } else {
    console.log(number)
  }
})

Classes in Node

Most people prefer coding in Node who came from front-end or Java background. Let’s take a look at the OOP way to inherit in Node:

class Auto {
  constructor({make, year, speed}) {
    this.make = make || 'Tesla'
    this.year = year || 2015
    this.speed = 0
  }
  start(speed) {
    this.speed = speed
  }
}
let auto = new Auto({})
auto.start(10)
console.log(auto.speed)

The way class is initialized (new Auto({})) is similar to a function call in the previous section, but here we pass an object instead of three arguments. Passing an object is a better pattern since it’s more versatile.

Interestingly with functions, we can create named functions (example above) as well as anonymous classes by storing them in variables (code below):

const Auto = class {
  ...
}

The methods like the one called start in the snippet with Auto are called prototype or instance method. As with other OOP languages, we can create a static method. They are useful when methods don’t need access to an instance.

Let’s say you are a starving programmer at a startup. You saved $15,000 from your meager earning by eating ramen noodles. You can check if that is enough to call a static method Auto.canBuy and there’s no car yet (no instance).

class Auto {
  static canBuy(moneySaved) {
    return (this.price<moneySaved)
  }
}
Auto.price = 68000

Auto.canBuy(15000)

To extend a class, let’s say our automobile is a Model S Tesla, there’s extends operand. We must call super() if we overwrite constructor(). In other words, if you extend a class and define your own constructor/initializer, then invoke super to get all the things from the parent (Auto in this case).

class Auto {
}
class TeslaS extends Auto {
  constructor(options) {
    super(options)
   }
}

To make this beautiful, define an interface, i.e., public methods and attributes/properties of a class. This way the rest of the code can stay ugly and/or change more often without causing any frustration or anger to developers who used the private API.

Node/JavaScript is loosely typed, hence, you need to put extra effort into documentation than you would normally do when creating classes in other languages with strong typing. Good naming is part of the documentation. For example, we can use ‘_’ to mark a private method:

class Auto {
  constructor({speed}) {
    this.speed = this._getSpeedKm(0)
  }
  _getSpeedKm(miles) {    
    return miles*1.60934
  }
  start(speed) {
    this.speed = this._getSpeedKm(speed)
  }
}
let auto = new Auto({})
auto.start(10)
console.log(auto.speed)

All the things related to modularizing described in the section on functions apply to classes. The more granular and loosely coupled the code, the better.

You might wonder, when to use a function and when a class. It’s more of an art than a science. It also depends on your background. If you spent 15 years as a Java architect, it’ll be more natural for you to create classes. You can use Flow or TypeScript to add typing. If you are more of a functional Lisp/Clojure/Elixir programmer, then you’ll lean towards functions.

Recommendation

Assume that all the code is written to be changed. Separate things which change more often (private) from other things. Expose only interfaces (public) and make them robust to changes as much as possible.

Lastly, have unit tests. They will serve as documentation and also make your code more robust. You will be able to change the code with more confidence once you have a good test coverage & keep on Nodding!


Looking for amazing apps built using Node.js? Click Here

Image Credits: hakernoon.com, jetbrains.com & enlight.nyc