DEV Community

Michael Otu
Michael Otu

Posted on

JavaScript Essentials: Part 7

This is the 7th part of this JavaScript Series (as a part of the whole) and in this part, we will look at how to break down our projects into small pieces so they are manageable. We will create some sort of separation of concerns, making our project appealing and easy to navigate. In all things, there are a beautiful part and of course the ugly part. So, don't overdo it. Do it when it needs to be done.

As mentioned earlier, our focus here is to break some part of our project into a separate file, export it, and then import it into our "main app". There are at the moment two ways to do this in JavaScript. Using the commonjs approach and also the ES6's modular approach. They are all great and we will look at both.

CommonJs

The import and export with commonjs is the default when not specified. That is how we could do,const readline = require( "readline" );.readlineis a built-in package. We can use this approach on the third-party packages or modules written in our project.

  • Import with commonjs is done withconst someVarNameForTheModule = require( "modNameOrPath" );.
  • We export by doing,module.exports = thingToExportOrStructuredObjectToExport.

Project

Let's kick off with a project to perform some math. We will create functions to add and subtract. Just these two.

  • Create a project folder,cmodule:cd ~/Projects && mkdir cmodule && cd cmodule
  • Initial the node project by doing,npm init -y
  • You can choose to add,"type": "commonjs"to thepackage.jsonfile. I am saying you can choose because that is the default.
{
"name":"cmodule",
"version":"1.0.0",
"main":"index.js",
"type":"commonjs",
"scripts":{
"test":"echo\ "Error: no test specified\ "&& exit 1 "
},
"keywords":[],
"author":"",
"license":"ISC",
"description":""
}
Enter fullscreen mode Exit fullscreen mode
  • Create two files,lib.jsandmain.js:touch lib.js main.js
  • Implement the body for theaddfunction in thelib.js
functionadd(x,y){
// return the sum of x and y
}
Enter fullscreen mode Exit fullscreen mode
  • Now that we have the function implemented, we have to export it to be used in ourmain.js.To export, we usemodule.exports = functionName.In our case, we domodule.exports = add.
module.exports=add;
Enter fullscreen mode Exit fullscreen mode
  • Here the entirety oflib.jsis just the add function. We exportedlib.jsas theaddfunction. So we can import it as,const someName = require( "./lib" );
  • In themain.js,we will import thelib.jsfile and make use of theaddfunction.
constlib=require("./lib");
// we did, "./lib", "dot slash lib", because main.js and lib.js are in the same folder.

console.log(lib(1,2));
Enter fullscreen mode Exit fullscreen mode
  • Let's add the subtraction function
functionsub(x,y){
// returns the difference x and y
}
Enter fullscreen mode Exit fullscreen mode
  • You are supposed to implement these functions yourself 🙂

  • The question is, how do we exportsub?Try it and access it insidemain.js

  • Know that, when we do,module.exports = X,Xis exported as a whole module so when we importconst moduleName = require( "moduleName" );,we directly get access toX.So we can not export another value with this same approach.

  • In a case such as this, we can export bothaddandsubby exporting them as a group (object).

module.exports={add,sub};
Enter fullscreen mode Exit fullscreen mode
  • Now when we import inmain.jswe can do
constlib=require("./lib");
// lib is an object so we can do lib dot someThing

console.log(lib.add(1,2));
console.log(lib.sub(1,2));
Enter fullscreen mode Exit fullscreen mode
  • Thelibmodule is exported as an object so we can do,moduleName.addandmoduleName.sub.

  • We can also import by doing by destructuring,const { add, sub } = require( "./lib" );

const{add,sub}=require("./lib");

console.log(add(1,2));
console.log(sub(1,2));
Enter fullscreen mode Exit fullscreen mode
  • There is another way to do multiple exports
exports.add=functionadd(x,y){
// return the sum of x and y
};

exports.sub=functionsub(x,y){
// return the difference of x and y
};
Enter fullscreen mode Exit fullscreen mode

Or

exports.add=function(x,y){
// return the sum of x and y
};

exports.sub=function(x,y){
// return the difference of x and y
};
Enter fullscreen mode Exit fullscreen mode
  • exports.alias = someThingandexports.someThing= someThingor also works likemodules.exports = { someThing }.I'd usually choose theexports.alias = someThingbecause, themodule.exports = {... }could extra lines.

ES Module

The import and export with the ES module style is not the default currently and as such must be specified explicitly by setting thetypeproperty to"module"in thepackage.jsonfile. In this case, we would be able to do,import readline from "readline";instead ofconst readline = require( "readline" );.We replaced theconstwithimport,the=andrequirewithfrom.

  • ES module import is done withimport someVarNameForTheModule from "modNameOrPath";.
  • We export by doing,export default thingToExportOrStructuredObjectToExportorexport thingToExportOrStructuredObjectToExport.

Project

We will build a similar project using the ES module style of import and export. We will create functions to add and subtract just as we did previously. So you can copy and paste it this time.

  • Create a project folder,emodule:cd ~/Projects && mkdir emodule && cd emodule
  • Initial the node project:npm init -y
  • Add,"type": "module"to thepackage.jsonfile.
{
"name":"emodule",
"version":"1.0.0",
"main":"index.js",
"type":"module",
"scripts":{
"test":"echo\ "Error: no test specified\ "&& exit 1 "
},
"keywords":[],
"author":"",
"license":"ISC",
"description":""
}
Enter fullscreen mode Exit fullscreen mode
  • Create two files,lib.jsandmain.js:touch lib.js main.js

  • Implement the body for the add in thelib.js

functionadd(x,y){
// return the sum of x and y
}
Enter fullscreen mode Exit fullscreen mode
  • Now that we have theaddfunction implemented, we have to export it to be used in ourmain.js.To export, we can useexport default functionName.In our case, we doexport default add.
exportdefaultadd;
Enter fullscreen mode Exit fullscreen mode
  • We could have also done
exportdefaultfunctionadd(x,y){}
Enter fullscreen mode Exit fullscreen mode
  • Here the entirety oflib.jsis just the add function. We exportedlib.jsas theaddfunction. So we can import it as,import someName from "./lib";
  • In themain.js,we will import thelib.jsfile and make use ofaddfunction.
importlibfrom"./lib";
// we did, "./lib" because main.js and lib.js are in the same folder.

console.log(lib(1,2));
Enter fullscreen mode Exit fullscreen mode
  • Let's add the subtraction function
functionsub(x,y){
// returns the difference x and y
}
Enter fullscreen mode Exit fullscreen mode
  • The question is, how do we exportsub?
  • In a case such as this, we can export bothaddandsubby exporting them as a group (object).
exportdefault{add,sub};
Enter fullscreen mode Exit fullscreen mode
  • Now when we import inmain.jswe can do
importlibfrom"./lib.js";

console.log(lib.add(1,2));
console.log(lib.sub(1,2));
Enter fullscreen mode Exit fullscreen mode
  • We can also import by doing,import { add, sub } from "./lib";
import{add,sub}from"./lib";

console.log(add(1,2));
console.log(sub(1,2));
Enter fullscreen mode Exit fullscreen mode
  • There is another way to do multiple exports
exportfunctionadd(x,y){
// return the sum of x and y
}

exportfunctionsub(x,y){
// return the difference of x and y
}
Enter fullscreen mode Exit fullscreen mode

Or

exportconstadd=function(x,y){
// return the sum of x and y
};

exportconstsub=function(x,y){
// return the difference of x and y
};
Enter fullscreen mode Exit fullscreen mode
  • With this sort of approach, it is either, we bundle the whole exports as one import or access individual imports one by one
import*aslibfrom"./lib.js";

console.log(lib.add(1,2));
console.log(lib.sub(1,2));
Enter fullscreen mode Exit fullscreen mode

OR

import{add,sub}from"./lib.js";

console.log(lib.add(1,2));
console.log(lib.sub(1,2));
Enter fullscreen mode Exit fullscreen mode

Summary

To use commonjs or es module import and export style is relative. commonjs comes with no configurations, so one would ask why not use it as is?module.exports = someObjectis the same asexport default someObject.We can import withconst someObject = require( "pathToModule" );andimport someObject from "pathToModule";.I like said, whichever you choose is okay. You can have a module/default export and also individual exports in the same file.

These are some rules that I try to stick to when I am developing my backend projects:

  • I avoid default export or module exports as much as possible and use the individual object export
  • If I have a controller, I use a default/module export without exporting anything else from that same file. So whenever I usemodule.exportsorexport default,I don't do any other export from the same file
  • I either use an object to group my constants and default export it or I would do individual exports of all the constants.
  • We can export objects without naming them and it works fine with the alias (name you give it) but I prefer to name my exports

Can you guess what is next? Well, we'd start doing some backend magics. We will start backend development.

Side project

If it challenges you, rewrite the mastermind program using multiple files. While on the topic I will challenge you. Complete this project. Either rewrite it for it to work or do whatever you have got to do to make it work and apply today's lesson.

/**
* Simple User Authentication Logic
*
* Concepts Covered:
* -> Variables
* -> functions
* -> conditionals
*
* Description
* -> Create a login system where users can sign up with an email and password, and then attempt to log in.
* -> Store user data in memory (using an array or object), and compare the entered credentials with the stored ones during login.
*
* Challenge
* -> Implement password validation with the following rules. Password must:
* - not be null or empty, hence, required
* - be at least six characters
* - must have at least one of the special characters:!, @, $, _, -
* - have at least one uppercase
* - have at least one lowercase
* - have at least one number
* -> Implement email validation with the following rules. Email must:
* - not be null or empty, hence, required
* - not exceed 256 characters in length
* - not contain prohibited characters (spaces, quotes, parentheses, brackets, comma, semicolon, colon, exclamation)
* - have a valid syntax (local part + "@" + domain + "." + tld)
* - not have 'email' in it
* - Local Part (Username):
* - Maximum length: 64 characters
* - Domain:
* - Maximum length: 253 characters
* - TLD (Top-Level Domain):
* - Must be one of the recognized TLDs (e.g.,,.org,.net, etc.)
* - Maximum length: 6 characters
*/

constreadline=require("readline");
constcrypto=require("node:crypto");

// users will an object with the key as the email because we intend to make the email unique
// another approach is to generate some random string for the key (the key must be unique)
// so that in the USERS object we can have { keys: {email, password},...}
// another approach is to use an array where we'd have [{email, password},...]
// try both approaches and let us know what is best
constUSERS={};

// expected actions are either login or signup
constAUTH_ACTIONS={
LOGIN:"login",
SIGNUP:"signup",
};

// We are expecting that the user input will indicate what action to be performed
// and the data to be used.
// Eg: [action] [email] [password]
asyncfunctiongetUserInput(question){
constReadLine=readline.createInterface({
input:process.stdin,
output:process.stdout,
});

returnawaitnewPromise((resolve)=>{
ReadLine.question(question,(answer)=>{
resolve(answer);
ReadLine.close();
});
});
}

// We expect the user to enter-> [action] [email] [password]
functionisValidInputFormat(input=""){
returninput.split("").length===3;
}

// This function should return an object with the property, action, email and password
// We could also have done, { action, data: { email, password } }
functiondestructureInput(input=""){
const[action,email,password]=input.split("");
return{
action,
email,
password,
};
}

functionisValidationAction(action=""){
returnaction&&Object.values(AUTH_ACTIONS).includes(action);
}

asyncfunctionsleep(ms){
returnnewPromise((resolve)=>setTimeout(resolve,ms));
}

// will return undefine or the user object
functionfindUserByEmail(email){
returnUSERS[email];
}

// hashes the password using sha256, we could use something better
// usually something like bcrypt, argon, etc
functionhashPassword(password){
returncrypto.hash("sha256",password);
}

functionisValidHash(password,passwordHash){
returnhashPassword(password)===passwordHash;
}

// usually, password validators will return a boolean, whether the password is valid or not
// however, we should consider adding an error message too
functionisValidPassword(password=""){
// not be null or empty, hence, required
if(!password||password===""){
return{
isValid:false,
message:"Password is required",
};
}

// TODO: Complete the body

// at this point, we've met all the rules we've set however there are some values
// that will pass through because of the approach we took
return{
isValid:true,
message:"",
};
}

functionisValidEmail(email=""){
// - not be null or empty, hence, required
if(!email||email===""){
return{
isValid:false,
message:"Email is required",
};
}

// TODO: Complete the body

return{
isValid:true,
message:"",
};
}

// When signing up, we have to make sure that the email doesn't exist
// The email and password are valid
// Then generate a key that hasn't been used and create an object
// and assign an object of the credentials as a value
// Log that signup is successful and log the key and email of the user
functionsignUpLogic({email,password}){
constuser=findUserByEmail(email);
if(user){
return{
success:false,
message:"User with such email already exists",
};
}

constpasswordHash=hashPassword(password);

USERS[email]={email,password:passwordHash};

return{
success:true,
message:"User created successfully, you can now login",
};
}

// When logging in, we have to make sure that the email exists
// We have to validate the email and password
// Opinion here: I learned from one of the devs that there is no need to
// validate data that you are not going to write to the DB
// What do you think
functionloginLogic({email,password}){
constuser=findUserByEmail(email);
if(!user){
// here the message could just be invalid credentials
return{
success:false,
message:"User with not found",
};
}

if(!isValidHash(password,user.password)){
return{
success:false,
message:"Invalid credentials",
};
}

return{
success:true,
message:`${user.email}has logged in`,
};
}

asyncfunctionApp(){
console.clear();
console.log("Running authentication app");

constuserInput=awaitgetUserInput("\n$");

if(["help","h","about"].includes(userInput)){
console.log(
"About: Simple User Authentication Logic"+
"\n- Expected format->[action] [email] [password]"+
"\n\t[action] can be 'signup' or 'login' followed by email and password"+
"\n"+
"\n- Expected format->[action]"+
"\n\t[action] can be 'exit', 'quit', 'list', 'l, 'about', 'help', or 'h'"+
"\n\t\t- [exit|quit]: exits or quits the program"+
"\n\t\t- [list|l]: list the user emails available"+
"\n\t\t- [about|help|h]: displays the about page"
);
return;
}

if(["list","l"].includes(userInput)){
console.clear();
console.log("Users\n-----");
for(constuserofObject.values(USERS)){
console.log(`${user.email}`);
}
return;
}

if(["exit","quit","\n"].includes(userInput)){
console.clear();
console.log("Application ended");
process.exit();
}

if(!isValidInputFormat(userInput)){
console.log(
"FormatError: Invalid date format. Expected format->[action] [email] [password]"
);
return;
}

const{action,email,password}=destructureInput(userInput);

if(!isValidationAction(action)){
console.log("FormatError: Invalid action. Expected->login|signup");
return;
}

constemailValidation=isValidEmail(email);
if(!emailValidation.isValid){
console.log(emailValidation.message);
return;
}

constpasswordValidation=isValidPassword(password);
if(!passwordValidation.isValid){
// It is not a good idea to trust what API (message) you are using, especially if the
// message is from a vendor (a 3rd party and the error messages aren't predefined)
// Usually, I'd just say invalid credentials or throw some error that the client can catch
// and knowing the error type, they'd be certain of what to do based on the rules
// the client has set (just print everything out, all the rules regarding a valid password)
// And most vendors will just return true or false but in our case, we'd add a message
console.log(passwordValidation.message);
return;
}

// had we several actions, we could use the switch instead
if(action===AUTH_ACTIONS.LOGIN){
// console.log( "We are doing a login" );
const{success,message}=loginLogic({email,password});
if(!success){
console.log(message);
return;
}

console.log(message);
}elseif(action===AUTH_ACTIONS.SIGNUP){
// console.log( "We are doing a signup" );
const{success,message}=signUpLogic({email,password});
if(!success){
console.log(message);
return;
}

console.log(message);
}else{
console.log(
`FormatError: Action must be one of${Object.values(AUTH_ACTIONS)}`
);
}
}

(async()=>{
while(true){
// Run the App function
awaitApp();
awaitsleep(5000);
}
})();
Enter fullscreen mode Exit fullscreen mode

Top comments(0)