Starting a new project can be a time-consuming process, especially when it comes to setting up the server. You might find yourself copying and pasting code from previous projects, trying to recall the purpose of each piece and customizing it for your current project. However, what if you had a reliable starting point that you could use for all your projects, allowing you to focus on the unique aspects of each project instead?
That’s where my Express setup comes in. I’ve compiled a list of best practices and created a setup that I use for all my projects. It’s available on GitHub, and all you need to do is fork the repository to get started in just 10 seconds.
This setup includes the following features:
- Basic server setup with database connectivity
- Global error handling
- JSON and URL encoded payload parsers
- Gzip compression middleware for server responses
- CORS middleware
- Middleware for serving static files
- Logging for both HTTP requests and server’s internal actions
- ESLint configuration for Node.js with the Airbnb JavaScript style guide
- Prettier configuration
- .gitignore configuration
- Development script that restarts the server upon file changes
With this Express setup, you’ll save time not only on this project, but on all future projects as well. You won’t have to spend time trying to remember code from months or years ago, or spend hours configuring packages and dependencies. Instead, you can immediately start coding to meet the specific requirements of your project. And, you’ll never have to feel guilty about relying on your preferred editor (khm… khm… VS Code) again!“
Basic setup
To begin, let’s create an empty folder for our application, navigate inside it, and run the npm init command. This will prompt you with some basic questions, and after filling them out, the package.json file will be created.
Next, let’s create a config.js file in the root directory that will serve as a centralized location for all the configs we will use in our application. We will export it so it will be available to all other files.
module.exports = {
node_environment: process.env.NODE_ENV,
server: {
port: process.env.SERVER_PORT,
},
database: {
connection_string: process.env.DATABASE_CONNECTION_STRING,
},
};
Note that the config values are stored as environment variables, so they will not be exposed publicly.
In order for the server to use these environment variables, we need to create a .env file where we will keep all our confidential information (passwords, secret keys, API tokens, etc.). So, let’s create a .env file in the root directory and add our configs there:
# Server config
SERVER_PORT = 'xxx'
# Node config
NODE_ENV = 'xxx'
# Database config
DATABASE_CONNECTION_URL = 'xxx'
Now, let’s create the app.js file, which will be the main file of our application and where we will build the server.
require('dotenv').config();
const express = require('express');
const http = require('http');
const mongoose = require('mongoose');
const Config = require('./config');
mongoose.connect(Config.database.connection_string).then(() => {
console.log('Successfully connected to database.');
const app = express();
// Catch 404 and forward to error handler
app.use((req, res, next) => {
const error = new Error('Endpoint not found.');
error.status = 404;
next(error);
});
// Error handler
app.use((error, req, res, next) =>
res.status(error.status || 500).json({
message: error.message,
error: Config.node_environment === 'development' ? error : {},
title: 'Error',
})
);
const server = http.createServer(app);
server.listen(Config.server.port || 3000, () => {
console.log(`Server is listening on port ${server.address().port}`);
});
});
With the code above, we have completed our basic setup. We used the popular third-party package dotenv to load all our environment variables from the .env file. Then we connected to the database (MongoDB in this example) and configured our server. We added a middleware that will catch all 404 errors and pass them to the global error handler, and we configured a global error handler that will send descriptive error messages only in development mode. After that, we created and built the server, which is now listening on the port specified in our configs. To start the server, you can run the npm start command in the terminal.
To install the
dotenvpackage in your app, you can runnpm i dotenvin the terminal.
Adding JSON and URL encoded payload parsers
To allow the server to parse the payload from requests, we need to configure the JSON and URL encoded parsers. This can be done with just two lines of code in our app.js file, after the initialization of the app variable:
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
This will enable both the JSON and URL encoded parsers for every request that comes to your server.
Adding gzip compression
An easy way to improve the performance of your app is to add compression to the responses from your server. Compressed data can be up to 80% smaller than the original data, which means less data is transmitted through the network, resulting in improved performance for your app. In Node.js, you can easily add a compression layer by installing the popular compression package. You can add it as a middleware in the app.js file, just after the initialization of the app variable:
const compression = require('compression');
...
const app = express();
app.use(compression());
app.use(express.json());
app.use(express.urlencoded({ extended:true }));
To install the
compressionpackage in your app, you can runnpm i compressionin the terminal.
With this, all responses from your server will be compressed before being sent to the client.
Adding CORS logic
You may encounter issues where your browser rejects requests to your server due to cross-origin resource sharing (CORS) policies. Before sending a request to the server, the browser will send something called a preflight request. The server must first respond to this preflight request in order for the browser to proceed with the original request.
We can easily configure CORS logic in our server that will properly handle these preflight requests and allow clients to proceed with the original requests. To do this, we will install the third-party cors package and add it as a middleware in our app.js file, just after the initialization of the app variable:
const cors = require('cors');
...
const app = express();
app.use(cors());
app.use(compression());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
To install the
corspackage in your app, you can runnpm i corsin the terminal.
With this, we are finished configuring our CORS logic, and browsers can call our server without any issues.
Serving static files
It is common for servers to serve static files such as images, videos, PDFs, CSS files, and JavaScript files. To serve static files, we can leverage the built-in static middleware by adding it in our app.js file just after the initialization of the app variable:
const path = require('path');
...
const app = express();
app.use(compression());
app.use(express.json());
app.use(express.urlencoded());
app.use(express.static(path.join(__dirname, 'public')));
In the code above, we specified that we want to serve everything from the public folder in our root directory as static files. You can change the folder name, but public is the conventional one.
Adding logger
One of the most important aspects of any server is logging. There are two types of logging that are particularly important: HTTP requests and the server’s internal actions (custom logs). Let’s start by creating our logger for custom logs. Create a helpers folder in the root directory and a logger.js file inside with the following content:
const winston = require('winston');
const Config = require('../config');
// Levels: ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']
const logger = winston.createLogger();
const customFormat = winston.format.printf(
({ level, message, timestamp, ...metadata }) =>
`${level}: ${message}\n${metadata && Object.keys(metadata).length ? JSON.stringify(metadata, null, 4) : ''}`
);
// If we're not in production then we will log to the console with the format:
// `${info.level}: ${info.message} {...rest}`
if (Config.node_environment !== 'production') {
logger.add(
new winston.transports.Console({
format: customFormat,
})
);
}
module.exports = {
logger,
};
Here we created and exported our custom logger, which we can use anywhere in the application. We used the popular third-party package winston and configured our custom logging format. In addition, we added console logging if we are not in a production mode (to make debugging easier during development). Our custom logging format will look like this:
info: Server is listening on port 3000
To install the
winstonpackage in your app, you can runnpm i winstonin the terminal.
Now, let’s import our logger in the main app.js file and replace all console.log() calls with our custom logger.
const { logger } = require('./helpers/logger');
...
server.listen(Config.server.port || 3000, () => {
logger.log('info', `Server is listening on port ${server.address().port}`);
});
Now, let’s add HTTP logs. To do this, we will use another popular third-party package called morgan. We can include it as a middleware in our app.js file just after the initialization of our app variable. This will intercept and log all requests.
const httpLogger = require('morgan');
...
const app = express();
if (Config.node_environment === 'development') {
app.use(httpLogger('dev'));
} else {
app.use(
httpLogger(
':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :response-time ms',
{ stream: { write: (message) => logger.log('info', message.trim(), { tags: ['http'] }) } }
)
);
}
To install the
morganpackage in your app, you can runnpm i morganin the terminal.
In the code above, we specified that we want only basic logs during development, and comprehensive logs during production. With this, we have finished setting up the logs for our server.
Configuring ESLint with Prettier
Using ESLint can help improve the quality of your code by ensuring best practices are being followed and potential bugs are avoided. In addition to ESLint, Prettier can be used to format code in a consistent manner, which is particularly useful when multiple people are working on the same project.
To configure ESLint and Prettier, we should first install a few packages as development dependencies. You can open the terminal and run the following command:
npm i eslint prettier eslint-config-prettier --save-dev
To configure ESLint, you can run the following command in the terminal:
npx eslint --init
This will prompt you with a few questions, to which you can answer as follows:
√ How would you like to use ESLint? · style
√ What type of modules does your project use? · commonjs
√ Which framework does your project use? · none
√ Does your project use TypeScript? · No
√ Where does your code run? · node
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · airbnb
√ What format do you want your config file to be in? · JSON
Here, we specified that we want ESLint to use the Airbnb JavaScript style guide. There are a few other style guides available, but Airbnb is widely regarded as the most comprehensive. This will add a few configuration files to our project, which we can customize further.
One of the files we want to customize is .eslintrc.json, which represents the configuration file for ESLint. Open that file and update it like this:
{
"env": {
"commonjs": true,
"es2021": true,
"node": true,
"mongo": true,
},
"extends": [
"eslint:recommended",
"airbnb-base",
"prettier",
],
"parserOptions": {
"ecmaVersion": "latest",
},
"rules": {}
}
This will extend ESLint to use Prettier as a formatter.
To configure Prettier, create a .prettierrc.json file in the root directory and update it as follows:
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always",
"embeddedLanguageFormatting": "off"
}
These are some basic configurations that will tell Prettier how to format our code. For the full list of Prettier configurations, you can check their official config page.
Now, let’s create a .eslintignore file in the root directory, which will tell ESLint which files and folders it should ignore.
/node_modules
Similarly, create a .prettierignore file in the root directory, which will tell Prettier which files and folders it should ignore.
/node_modules
package.json
package-lock.json
With this, we have finished setting up ESLint and Prettier, ensuring that our project is ready to be coded in a consistent and quality manner.
Adding .gitignore config
To ensure that certain files and directories are excluded from Git commits, we can create a .gitignore file in the root directory.
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Environment variables file
.env
With the code above, we have ensured that our environment variables won’t be included in commits (which is a critical security issue), as well as all installed node modules and caching files that may be created by NPM and ESLint.
Configuring development script
To increase productivity, we can create a script that automatically restarts the server whenever code changes are made. To do this, we can install the third-party package nodemon as a development dependency by running the following command:
npm i nodemon --save-dev
Next, open the package.json file and add the following script under the scripts property:
"scripts": {
"start-dev": "nodemon app.js"
},
Now, we can run the server using the npm run start-dev command and it will automatically restart the server whenever code changes are made.
The big picture: How this server setup will benefit you in the long run
To wrap up, we have seen how we can create a basic Express setup that can be used for all our projects. This setup includes features such as a basic server setup with a connection to the database, a global error handler, JSON and URL encoded payload parsers, Gzip compression middleware for server responses, CORS middleware, middleware for serving static files, a logger for both HTTP requests and server’s internal actions, an ESLint configuration with the Airbnb JavaScript style guide, a Prettier configuration, a .gitignore file, and a development script that will automatically restart the server when any file changes.
With this Express setup, you can save time not just on this project, but on all future projects as well. You won’t have to spend time trying to remember code you wrote a year ago or spend hours configuring packages and dependencies. You can just start coding for your project requirements right away.
If you want to use this setup for your own projects, you can find it on my GitHub repository. Simply fork the repository and you’ll be ready to start building your new app in just a few seconds.


