NodeJS Typescript Production grade setup template

11 min read Original article ↗

Shuvrojit Biswas

You can also read the story for free here.

In this article, we’re going to build a production grade setup template for nodejs with typescript. We’ll also cover linting, formatting, building, running and testing the project in a production-ready way. We’ll explore each tool, the problems they solve, and why they’re essential for building a production-grade setup. We will use tools like eslint, prettier, pm2, cross-env to make sure our starter template looks more and more production grade and works on all platforms. So let’s dive in.

Let’s start by creating the project directory. You can use the mkdir command or any graphical file manager based on your preference. Afterward, we’ll initialize a Git repository to track project changes over time. For managing packages, we’ll use Yarn, although you can choose between npm and Yarn based on your preference. In this project, we’ll go with Yarn due to its performance and enhanced feature set. We’ll also use the -y flag to automatically generate the necessary project files with default values.

# create direcotry
mkdir node-boilerplate

# change directory
cd node-boilerplate

# initialize git repo
git init

# initialize yarn
yarn init -y

Now that we’ve set up the basic structure, let’s move on to configuring the tools needed to ensure a smooth development workflow.

Typescript

Next, we’ll set up TypeScript. Since Node.js cannot directly run TypeScript files, we need a compiler to convert them into JavaScript. TypeScript comes with its own compiler called tsc, which handles the conversion from TypeScript to JavaScript. We’ll use tsc to build the project later, but for now, let’s focus on setting up the development environment.

During development, constantly restarting the server after each change can be tedious. To solve this, we use file watchers like nodemon, which automatically restart the server when file changes are detected. Additionally, we’ll use ts-node, a tool that allows us to run TypeScript code directly in Node.js without needing to compile to JavaScript first.

When working with TypeScript, you’ll need type definitions for each package you use. Some packages come with built-in type definitions, while others do not. For packages without type definitions, you can find them on DefinitelyTyped. Most type definition packages follow a standard naming convention: @types/package-name. For example, the type definitions for Node.js are available as @types/node .

To get started, we’ll install the following packages as development dependencies:

  • nodemon: For monitoring file changes and automatically restarting the server.
  • typescript: The TypeScript compiler, tsc, which compiles TypeScript into JavaScript.
  • ts-node: To run TypeScript directly in Node.js during development.
  • @types/node: TypeScript type definitions for Node.js, enabling better type-checking and autocompletion.

Before we continue, it’s important to understand the difference between dev dependencies and production dependencies. Dev dependencies are packages required only during development and testing phases, not in production. They help keep your production build lean and focused on runtime essentials, reducing potential vulnerabilities and improving performance. When using Yarn, you can install a package as a dev dependency by adding the -D flag.

Install them with the following command:

yarn add -D nodemon typescript ts-node @types/node

With these packages installed, we’re ready to configure our development environment and streamline the process.

To use TypeScript effectively, we need to create a configuration file (tsconfig.json). You can either manually create and configure this file or generate a default one using the TypeScript compiler. To do this, run the following command:

npx tsc --init

This will generate a default tsconfig.json file. While this file is comprehensive, most of its content will be commented out. TypeScript offers a wide range of configuration options, but for our project, we’ll focus on the most essential ones. Below is a recommended production configuration:

{
"compilerOptions": {
"target": "es6", // ECMAScript 6 for compatibility
"module": "commonjs", // CommonJS module system (used by Node.js)
"lib": ["es6"], // Include ECMAScript 6 library files
"allowJs": false, // Do not allow JavaScript files in TypeScript compilation
"outDir": "build", // Output compiled files to the build directory
"rootDir": "src", // Define the root directory for TypeScript files
"strict": true, // Enable strict type-checking
"noImplicitAny": true, // Disallow `any` types unless explicitly stated
"noUnusedLocals": true, // Warn about unused variables
"noUnusedParameters": true, // Warn about unused function parameters
"esModuleInterop": true, // Ensure compatibility with ES6 modules
"resolveJsonModule": true, // Allow importing JSON modules
"skipLibCheck": true, // Skip type-checking of declaration files
"sourceMap": false, // Do not generate source maps
"declaration": true, // Generate declaration files (`.d.ts`)
"declarationMap": true, // Create source maps for declaration files
"removeComments": true // Remove comments from the compiled JavaScript files
},
"include": ["src/**/*.ts", ], // Include source
"exclude": ["node_modules", "build"] // Exclude unnecessary directories
}

If you’d like to explore more about these configuration options, refer to the TypeScript documentation.

Now, let’s create a development command to run our TypeScript files with Nodemon. To do this, we’ll add a script in the package.json file. In Node.js, scripts are commonly used to automate commands such as starting a server, running tests, or building a project.

In the package.json file, add the following scripts section:

{
"scripts": {
"dev": "nodemon src/index.ts"
}
}

This script runs nodemon, which will automatically restart the server whenever changes are made to the src/index.ts file.

Next, create the src directory and add a new file named index.ts. Inside this file, write a simple console log statement:

// src/index.ts
console.log('Server is running');

Finally, run the following command to start the development server:

yarn dev

If everything is set up correctly, you should see the message “Server is running” printed in the console.

Linting and Formatting

To maintain code quality and consistency, we will integrate linting and formatting tools into our project. For this purpose, we will use ESLint for linting and Prettier for formatting.

Prettier is a tool that ensures a consistent code style throughout your project. Begin by installing Prettier with the following command:

yarn add -D prettier

Next, create two configuration files for Prettier:

  • .prettierrc: Defines Prettier’s formatting rules.
  • .prettierignore: Specifies files and directories that should be excluded from formatting.

In .prettierrc, you can include the following configuration:

{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80
}

You can learn more about these configuration options in the Prettier documentation.

To facilitate formatting through the command line, add these scripts to your package.json:

{
"scripts": {
"format": "prettier --check .",
"format:fix": "prettier --write ."
}
}
  • "format" checks the formatting of files without making changes.
  • "format:fix" automatically fixes formatting issues.

When working on a project, it’s crucial to maintain code quality and formatting standards. Manually formatting files before every commit can be tedious and prone to oversight. By using lint-staged in combination with husky, you can automate this process, ensuring that only the files you’re committing are formatted according to your style guidelines

Lint-Staged: Runs linters or formatters only on the files you’re about to commit. This way, you don’t need to format everything; just the files that are being changed.

Husky: Sets up Git hooks that run automatically at certain points, like before you commit. It makes sure that lint-staged runs and formats your files before your changes are committed.

Begin by installing lint-staged:

npx mrm@2 lint-staged

Add a lint-staged configuration to your package.json file:

{
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
}
}

**/*: Targets all files in your repository.

  • prettier --write: Runs Prettier to format these files.
  • --ignore-unknown: Prevents Prettier from failing on unsupported file types.

This setup ensures that files are automatically formatted according to your Prettier rules just before they are committed. This reduces the need for manual formatting and helps keep your code consistent.

For linting, ESLint helps identify and report on patterns in code to enforce coding standards. Install ESLint and necessary plugins:

yarn add -D eslint @eslint/js @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier

Create an ESLint configuration file named eslint.config.mjs in the project root and include the following setup:

import pluginJs from '@eslint/js';
import tsEslint from '@typescript-eslint/eslint-plugin';
import prettierConfig from 'eslint-config-prettier';

export default [
{ files: ['**/*.{ts,js}'] },
{ ignores: ['build', 'node_modules', 'coverage', 'jest.config.js', 'eslint.config.mjs'] },
pluginJs.configs.recommended,
...tsEslint.configs.recommended,
...tsEslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.url,
},
},
},
prettierConfig,
];

This ESLint configuration file sets up linting for both TypeScript and JavaScript. It applies recommended rules for JavaScript and TypeScript, ignores specific directories and files, and integrates Prettier for consistent code formatting. The configuration ensures that both JavaScript and TypeScript files are linted according to best practices while avoiding conflicts with Prettier.

If you’re encountering TypeScript-related issues while linting, you can remove the line:

...tsEslint.configs.recommendedTypeChecked,

This line enables type checking during linting, so removing it will disable type checking and may resolve related problems.

Finally, add these linting commands to your package.json scripts:

{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}
  • "lint" runs ESLint to check for code issues.
  • "lint:fix" automatically fixes linting errors where possible.

By integrating these tools, you’ll ensure that your code is both clean and consistent, streamlining development and maintaining high standards.

Building and Running the Project

With our linting and formatting tools in place, we can now focus on building and running the project. We’ve previously discussed using tsc for compiling TypeScript files, and now we'll put that into action.

To set up the build process, add a new script to your package.json that uses tsc to compile the TypeScript code:

{
"scripts": {
"build": "tsc"
}
}

This build script will compile your TypeScript files into JavaScript. When you run yarn build, a new directory called dist will be created, containing all the compiled JavaScript files.

For running the compiled JavaScript files in a production environment, using node dist/index.js is an option. However, this method has several drawbacks, such as the server stopping if the app crashes and the lack of automatic recovery. To address these issues, we'll use PM2, a process manager that provides a range of benefits:

  • Automatic Restarts: If the server crashes, PM2 will restart it automatically.
  • Scaling: PM2 allows you to run multiple instances of your application to handle more load.

Let’s download it:

yarn add pm2

When using pm2 for process management, cross-env can be very useful for setting environment variables consistently across different platform. cross-env ensures that environment variables are set consistently across different operating systems (like Windows and Unix-based systems) by standardizing their configuration, which helps avoid issues and maintain consistent behavior in your application.

yarn add -D cross-env

To configure PM2, you need an ecosystem.config.json file. Place this file in the root directory of your project with the following content:

{
"apps": [
{
"name": "app",
"script": "dist/index.js",
"instances": 1,
"autorestart": true,
"watch": false,
"time": true,
"env": {
"NODE_ENV": "production"
}
}
]
}

This configuration tells PM2 to:

  • Run the application from the compiled JavaScript file (dist/index.js).
  • Use one instance of the app.
  • Automatically restart the app if it crashes.
  • Not watch for file changes (set to false).
  • Include timestamps in logs (time set to true).
  • Set the NODE_ENV environment variable to production.

To start the application using PM2, add these scripts to your package.json:

{
"scripts": {
"start": "cross-env NODE_ENV=production pm2 start ecosystem.config.json --env production",
"start:prod": "cross-env NODE_ENV=production pm2 start ecosystem.config.json --no-daemon"
}
}
  • start: Launches PM2 with the ecosystem.config.json file and sets the environment to production.
  • start:prod: Starts PM2 in the foreground (useful for debugging).

By using PM2, you can ensure that your application runs smoothly in production, with automatic restarts and the ability to handle more load through scaling.

Testing

To ensure the reliability and functionality of your code, integrating testing into your development workflow is crucial. We’ll use Jest as our testing framework and ts-jest to handle TypeScript files.

Start by installing the necessary packages using Yarn:

yarn add -D jest ts-jest @types/jest coveralls 
  • jest: The core testing framework.
  • ts-jest: Allows Jest to work with TypeScript files.
  • @types/jest: Provides TypeScript type definitions for Jest.
  • coveralls: A tool for sending coverage reports to Coveralls, a service that tracks code coverage over time.

Next, configure Jest by adding the following lines to your jest.config.js:

/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': ['ts-jest', {}],
},
coverageReporters: ['text', 'lcov', 'clover', 'html'],
testMatch: ['**/tests/**/*.ts'],
};
  • testEnvironment: 'node': Sets the test environment to Node.js.
  • transform: Uses ts-jest to transform TypeScript files.
  • coverageReporters: Specifies the types of coverage reports to generate (e.g., text, HTML).
  • testMatch: Defines the pattern for locating test files.

Create a tests directory in your project root and start writing your test cases.

To run tests and view coverage reports from the command line, add these scripts to your package.json:

{
"scripts": {
"test": "jest -i --colors --verbose --detectOpenHandles",
"test:watch": "jest -i --watchAll",
"coverage": "jest -i --coverage",
"coverage:coveralls": "jest -i --coverage --coverageReporters=text-lcov | coveralls"
}
}
  • test: Runs Jest with options for color output, verbose logging, and detection of open handles.
  • test:watch: Runs Jest in watch mode, which reruns tests on file changes.
  • coverage: Generates a coverage report.
  • coverage:coveralls: Sends the coverage report to Coveralls. This command runs Jest with coverage reporting, converts the report to a format Coveralls understands (text-lcov), and pipes it to the Coveralls tool.

Coveralls is a service that helps track code coverage over time by integrating with your CI/CD pipeline. By sending your coverage reports to Coveralls, you can visualize coverage trends, identify areas of your codebase that need more testing, and ensure that your test suite remains comprehensive as your project evolves.

Integrating these tools will help maintain code quality and provide insights into your test coverage, leading to more robust and reliable software.

You can get the repo here: https://github.com/shuvrojit/node-boilerplate

Whoa! We’ve covered a lot of ground, but there’s more to come. Next up is setting up production-grade logging to keep track of what’s happening in your application. Stay tuned for more on this essential topic!