DEV Community

Chiranjib
Chiranjib

Posted on • Updated on

Setting up a Node.js backend

Previous: Learn to build with MERN stack

DISCLAIMER: This guide by no means aspires to be the holy grail. At the time of writing this I've been developing software for more than a decade and let's say I know of a few things that would make coding easy when it comes to basic principles like Readability, Maintainability, etc.
P.S. If you don't like my suggestions, please don't hurl abuses at me, I have my friends for doing that. If you have some constructive feedback, please do share!

Step 1 - start the project and install a package

Pick a folder on your device where you'd like to keep your code. It can be surprisingly easy to start a Node.js project. Open command terminal and navigate to the directory of your choice. Once there, execute this:

> npm init

Once you fill out the questionnaire, you should notice a package.json file in your directory. You may want to read more about npm, npmrc, package.json and package-lock.json here: About npm

To get our feet wet, let's install a package - winston

> npm install --save winston

Let's check our package.json

....
"dependencies": {
    "winston": "^3.8.2"
}
....
Enter fullscreen mode Exit fullscreen mode

Notice the Caret (^) next to the version. This indicates winston should have any package compatible with 3.8.2. In some cases, this may present an unwanted problem. If we run npm update, it can potentially upgrade the package to the next compatible version. I like to exercise a bit more control on the package and the versions I have. There are many ways to do this, I prefer using npmrc for this.

Let's create a file named .npmrc as a sibling of package.json and put this line in it:

save-exact=true
Enter fullscreen mode Exit fullscreen mode

If you want to check all the options available for this file, check out npmrc config options
Now, let's uninstall the package and install again

> npm uninstall --save winston
> npm install --save winston
Enter fullscreen mode Exit fullscreen mode

Notice the package.json has this now

....
"dependencies": {
    "winston": "3.8.2"
}
....
Enter fullscreen mode Exit fullscreen mode

Sweet! We have a solid grip on all the package versions.

Step 2 - installing a few essentials

> npm install --save express
> npm install --save-dev nodemon
> npm install --save-dev eslint
Enter fullscreen mode Exit fullscreen mode

If you notice, we've installed express with --save and the other two with --save-dev.
The idea is to have some packages that are only required in our local setup while writing code. When it's time to release to production, the final version can be leaner to minimize the size of the shipment (more on that later).

About the packages:

  • express : popular library to define endpoints
  • nodemon : popular for development as it watches files for changes and restarts the node process
  • eslint : library required for enforcing code style guidelines

Step 3 - set up linting

It's essential to have a few rules to keep the code style consistent, eslint helps us do that. Let's set it up, run this command

npx eslint --init

Answer the questions, specify your style of coding, and we're good to go. You may observe, a .eslintrc.js file would have gotten created (provided you picked JavaScript as your option instead of JSON or YAML)

Step 4 - create a few folders

For the sake of sanity and keeping things modular, let's create the following directory structure to begin with. Don't worry about the content yet, we will focus on this as we move along.

/
  - config
  - controllers
  - entities
  - utils
Enter fullscreen mode Exit fullscreen mode

Step 5 - preparing for env variables

It's a good practice to put all the variables in a single location, so that it's easier to refactor in the future. This may seem an overkill when we are just starting out, but as the application code grows and complexity increases, environment variables get messy and making changes can get tricky.
Let's create a file config/index.js and put the following content in it:

module.exports = {
    SERVER: {
        PORT: 3000,
        REQUEST_BODY_SIZE_LIMIT: '10mb'
    },
    LOGGING: {
        LEVEL: 'warn'
    }
};
Enter fullscreen mode Exit fullscreen mode

Step 6 - setting up project for absolute path

One thing that quickly gets ugly, are relative file paths (require ../../../folder1/folder2/file1.js). Nowadays, there are many ways to get rid of it, here we'll explore one option.

npm i --save link-module-alias

Edit the package.json to add a postinstall script and the _moduleAliases section as shown below:

...
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "postinstall": "link-module-alias"
    },
    "_moduleAliases": {
        "_controllers": "./controllers",
        "_entities": "./entities",
        "_utils": "./utils",
        "_config": "./config"
    }
....
Enter fullscreen mode Exit fullscreen mode

You may run npm i after this, and notice this in the console output:

....
> link-module-alias

link-module-alias: _controllers -> ./controllers, _entities -> ./entities, _utils -> ./utils, _config -> ./config
...
Enter fullscreen mode Exit fullscreen mode

Now, we may use these as normal require statements like require('_controllers') in any file within our project structure.

In this post, I am only trying to highlight that you SHOULD enable something like this, by no means am I preaching that you use the same package.

Step 7 - specify start scripts

We choose to run Node.js projects with npm commands instead of a simple command like node index.js for the sake of consistency. Let's inject the following scripts in the package.json

....
"scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node index.js",
        "dev": "nodemon index.js",
        "postinstall": "link-module-alias"
    }
....
Enter fullscreen mode Exit fullscreen mode

Step 8 - setting up the first endpoint

Let's try to set up a healthcheck endpoint that simply responds with text "OK". First, let's enable us to use the logger library we integrated earlier. Create a file utils/logger.js as shown below:

const winston = require('winston');
const { LOGGING: { LEVEL } } = require('_config');

const { combine, timestamp, prettyPrint, errors } = winston.format;

module.exports = winston.createLogger({
    format: combine(
        errors({ stack: true }),
        timestamp(),
        prettyPrint()
    ),
    level: LEVEL,
    transports: [
        new winston.transports.Console()
    ]
});

Enter fullscreen mode Exit fullscreen mode

Now, we can focus on our route. Let's edit the index.js file as

const { SERVER: { PORT } } = require('_config');
const logger = require('_utils/logger');

const express = require('express');

const app = express();

app.use((req, res, next) => {
    logger.log('info', `Received request ${req.originalUrl}`);
    next();
});

app.get('/healthcheck', (req, res, next) => {
    res.send('OK');
});

app.use('/', (req, res) => {
    res.send(`${req.originalUrl} can not be served`);
});

app.listen(PORT, () => {
    logger.log('info', `Listening on port ${PORT}`);
});

Enter fullscreen mode Exit fullscreen mode

Few highlights of the simple snippet above:

  • all the statements defined for app, get executed in sequence
  • we have defined a logger with winston that logs to the console for the sake of this example and restricted it to only warn level using an environment variable. The line logger.log('info'... won't print anything to the console until the level is set to 'info'. This comes in handy when in production we want to start printing more log statements to debug a problem. We can just change the environment variable on the fly.
  • app.use() statement in the beginning takes in a middleware that gets executed for any endpoint call that the app receives. This is useful for doing tasks like authentication, logging for audit, etc. The next() call is crucial here to pass on the request chain to the next applicable middleware or endpoint definition as applicable
  • app.get('/healthcheck') is the endpoint we intended to define, which simply responds with 'OK'
  • app.use('/') is the fallback endpoint, that would catch all the requests that could not be served, and we can show graceful error messages if we desire

To run the server, you need to execute either of the commands

  • npm run start
  • npm run dev

Voilà! We're all set! You may hit the endpoint we have just defined, and revel in the glory of your hard work.

Healthcheck

So, we have our boilerplate ready. Let's shape it up to be more robust and introduce modularity for more routes.

Next: Create modular routes with express

Top comments (0)