Setting up a Node.js backend
Chiranjib
Posted on February 14, 2023
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"
}
....
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
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
Notice the package.json has this now
....
"dependencies": {
"winston": "3.8.2"
}
....
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
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
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'
}
};
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"
}
....
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
...
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"
}
....
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()
]
});
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}`);
});
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 linelogger.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. Thenext()
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.
So, we have our boilerplate ready. Let's shape it up to be more robust and introduce modularity for more routes.
Posted on February 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.