Node.js - AdonisJS Photo by Clément H on Unsplash

Awesome AdonisJs & Best Practices #1

AdonisJs is a Node.js MVC framework that runs on all major operating systems. It offers a stable eco-system to write a server-side web application so that you can focus on business needs over finalizing which package to choose or not.

The purpose of this article is to show the best practices to improve productivity for  AdonisJs based business logics.

Asynchronous Functions

ES8 introduces Async Functions as a brand-new way to execute asynchronous jobs in JavaScript. Node.js v7.10+ supports async/await. Therefore, Async functions can be used in AdonisJs.

Traditionally, JavaScript is a single-thread language. So, running async jobs in the JS, is the strongest aspect to work with non-blocking I/O.

Asynchronous jobs in JavaScript can be done in 3 way:

  • Callbacks (Outdated -In This Scope)
  • Promises with then/catch/finally
  • Async/await for promises

You can read more about for these 3 different ways here.

Async/Await Hell. Credits freecodecamp

You should consider these two things while using async/await:

Async/Await Blocks Process in Favor of Promise

Await-ing an async function will make process to wait for the 'Promise' to resolve before continuing to execute rest of the code.

Errors Need to be Handled With Traditional Try/Catch

Simple async/await

async usingAwait(){
  await delay(1000);
} 

If an asynchronous function throws an error, the promise will be rejected. If the function returns a value, the promise will be resolved. Therefore, awaited function should be wrapped with a try/catch block. 

async usingAwaitwithTryCatch(){
  try{
    await delay(1000);
  }
  catch (e) {
    // handle the error
    console.log(e);      
  }
}

You can also promisify callbacks via the 'Promisify Helper'.

One more thing:

If you really don't need to wait for the result of a job, let it work in background asynchronously or use then/catch. Redundant 'async / await' blocks will disrupt asynchronous structure, which is one of the beautiful aspects of javascript, and will decrease your code efficiency.

Environment Variables

Environment variables are nice way to keep sensitive information out of the code base and easily configure the application. Therefore, refusing to write a piece of code can be turned into a nightmare by writing tens of lines of code.

An Environment File Starts With a '.' (dot)

 Environment variables solve various problems. Of course, environment variables are not belong only to AdonisJs. But AdonisJs has an 'Env' provider which is nice and elegant approach.
While working on an AdonisJs project, you may want to have some variables that dynamically change according to the environment. For example, you may want to set the database connection settings, or maybe want to specify the applications URL. AdonisJs deals with this issue through an '.env' file. This file allows you to keep different values for different environments (development, testing, production) that read by AdonisJs. This gives a very convenient way of manipulating the application's behaviors according to environment.

For example:
Providing a valid host address is a must while configuring database. So, instead of specifying it directly in the source code, writing it to the .env file allows us to use different database hosts that vary by environment.

// Production .env
DB_CONNECTION=mysql
DB_HOST=prod_ip_addr
DB_PORT=3306
DB_USER=prod_user
DB_PASSWORD=prod_user_pass
DB_DATABASE=prod_db
// Development .env
DB_CONNECTION=mysql
DB_HOST=dev_ip_addr
DB_PORT=3306
DB_USER=dev_user
DB_PASSWORD=dev_user_pass
DB_DATABASE=dev_db

You can easily reach environment variables with just a few line of codes:

// Create an 'Env' instance. 
const Env = use('Env');
// Get environment variables
let host = 
Env.get('DB_HOST');
let port = Env.get('DB_PORT');
let user = Env.get('DB_USER');
let password = Env.get('DB_PASSWORD');
let db = Env.get('DB_DATABASE');

Another nice thing is, say, if you want to change database host address, just making change to 'DB_HOST' key in environment file will be enough.

Last but not least, another benefit of environment files is that secrets can be kept safe with this way because of the golden rule "you must never ever commit this environment file to VCS".

For more information: https://adonisjs.com/docs/4.1/configuration-and-env#_env_provider

Validators

In computer science, data validation is the process of ensuring data have undergone data cleansing to ensure they have data quality, that is, that they are both correct and useful. It uses routines, often called "validation rules" "validation constraints" or "check routines", that check for correctness, meaningfulness, and security of data that are input to the system. The rules may be implemented through the automated facilities of a data dictionary, or by the inclusion of explicit application program validation logic.

- Wikipedia

AdonisJs allows us to easily verify the data via validators. Just determine the rules and AdonisJs will take care of the rest with the help of 'Indicative' which is a validation library. Don't worry, the documents and API reference of Indicative is quite clear!

Sample AdonisJS Route Validator

You can write rules just like writing in English. Create a rules object with the validation rules you determine and execute the validation.

const { validate } = use('Validator')
// Create rules object
const rules = {
email: 'required|email',
}
// Validate the data against to the rules.
const validation = await validate(data, rules)
// Check the fields that failed 
if (validation.fails()) {
// Code line
}

Route Validators

Traditionally, data validation is performed at the beginning of the controller methods. Adonis introduces a new approach, called Route Validators, for data validation. Route validators make data validation a bit more easier and so code base gets more structured and efficiency and readability increases.

Let's create a validator that validates email saved for newsletter.

adonis make:validator Newsletter

Adonis creates validators inside the app/Validators directory. 

// app/Validators/Newsletter
'use strict'
 
class Newsletter {
  get rules () {
    return {
      // validation rules
    }
  }
}
 
module.exports = Newsletter

Change returned rule object as:

return {
  email: 'required|email'
}

Chain the validator to the route:

Route.post('/newsletter', 'EmailController.newsletter').validator('Newsletter')

Now, when a request hit to /newsletter route, the Newsletter route validator will be triggered and the data in the request object will be tested against the rules.

Hence, with validators, you can build your business logic in a more refined and elegant way.

For more information: https://adonisjs.com/docs/4.1/validator

Database Hooks

A hook. Credits svgsilh.com

 

Database Hooks is a concept that is used to execute commands before or after a specific database operation. Hooks save you from writing same code over and over again.

For example, for security reasons, you must save the user password into the database in hashed format. Using hooks, you can be sure that the user password will be hashed before each creation.

class User extends Model {
  static boot() {
    super.boot();
    this.addHook('beforeCreate', async (user) => {
      user.password = await Hash.make(user.password);
    })
  }
}

Another example is that, again for security reasons, sometimes you may need to escape strings and in a such case, database hooks could be the best choice.

const he = require('he');
class User extends Model {
  static boot() {
    super.boot();
    this.addHook('afterFetch', async (data) => {
      data.key = he.escape(data.key);
    })
  }
}

 

Below is the list of all lifecycle events and their description (*):

EventDescription
beforeCreateBefore creating a new row.
afterCreateAfter a new record is created.
beforeUpdateBefore updating a row.
afterUpdateAfter a row has been updated.
beforeSaveThis event occurs before creating or updating a new record.
afterSaveAfter a new record has been created or updated.
beforeDeleteBefore removing a row.
afterDeleteAfter a row is removed.
afterFindAfter a single row is fetched from the database.
afterFetchAfter the fetch method is executed.
The hook method receives an array of the model instances.
afterPaginateAfter the paginate method is executed.
The hook receives two arguments,
where the first is the array of model instances
and 2nd the pagination metadata. 

(*): Lifecycle Events

For more information: https://adonisjs.com/docs/4.1/database-hooks

In Conclusion

AdonisJS has many beautiful features. We introduced some of them with some best practices, in this post.

To be continued...