Node Projects: Linting + Formatting + Conventional Commits
This article is part of a series which covers the setup of automated linting, formatting, and conventional commits in various JavaScript runtimes using git hooks.
- 1
Node Projects
What are we making?
We're going to setup a basic Node.js project with a linting and formatting automation that will use git hooks
to act upon staged files when we git commit
. We won't be installing any frameworks here as the purpose of this project is to illustrate the ease at which linting and formatting automations can be setup. As projects become more complex in their architecture, the need to extend the linting and formatting configurations may arise and this can be accomplished by ammending configuration rules or by importing preset configurations and plugins.
As an added extra, we can also use the same approach of git lifecycle automations to standardise commit messages with conventional-commit
syntax! There'll be a future article diving deeper into the concept of conventional commits and the potential it may have to add value to projects, but for now, we'll go through an optional example of how to set it up.
Why?
I'm a bit of a weirdo. I like to ensure that before my work is committed to git locally, staged files comply with my project's linting and formatting rules, there are no TypeScript compilation errors and commit messages are formatted in a consistent manner. I stop short of adding a test run locally because I'm just a weirdo, not a masochist.
We could rely upon IDE extensions to automatically format and lint as we progress through our work, but having a setup like this both acts as a fail-safe to ensure only compliant code is being committed, and makes configuration portable within the repo. Collaborators can automatically inherit the setup when the repo is cloned.
Together with IDE extensions, this workflow makes for a robust toolchain.
What are we using?
The key libraries are:
- typescript: Type-safe way of writing JavaScript
- eslint: For highlighting potential problems which may lead to errors within the runtime environment.
- prettier: For enforcing a common style for our source code to aid readability.
- husky: For accessing git hooks.
- lint-staged: For running commands against staged files.
- gitmoji-cli: An interactive terminal for formatting commits with emojis and conventional-commit syntax.
I don't have time to read! Is there a video I can watch or a repo I can clone?
I hear you, life is busy... I haven't made a video yet but here's a link to a demo repo that you can clone!
What are the steps?
I love the enthusiasm! Let's get started!
1. Initialise the project
node init -y && git init
In your terminal, create a new directory for your project and cd
into it. Use the node init -y && git init
command to initialise a new node project with git in the current directory.
Step 2 - Install Dependencies
Node doesn't ship with any linting or formatting libraries so we'll be installing the following as development dependencies:
npm install -D husky lint-staged prettier typescript @types/node
When you issue the above you may find your IDE indicated that a few hundred files have changed. Panic not! We just need to create a .gitignore
file and add our node_modules
directory to it. This can be done with the following command.
echo "node_modules" > .gitignore
A more robust way of creating a .gitignore for your project is to use the open source tool provided by Toptal which we can also use on the command line with the add-gitignore
package as follows:
npx add-gitignore node macos
We're using the node
template here because we're creating a node project. We're also using the macos
template here becuase this is the system being developed on. can be modified based upon the system being developed on. Linux and windows templatese are also available.
3. Initialise TypeScript
There's a few ways that we can configure TypeScript
within our project. The first is to initialise it using the tsc
cli by issuing npx tsc --init
.
This will generate a default tsconfig.json
configuration file, with some options chosen. See here for more information about these options.
Another way is to make use of preset base configurations which we can extend from. This is a great choice for those who aren't familiar with all of TypeScript's compiler options, and require some sensible defaults to start with.
We're going to use this second option and set up our TypeScript configuration to extend from the node-lts
base configuration.
npm install -D @tsconfig/node-lts
We can start by installing the base configuration as a development dependency, and then create a tsconfig.json
file with the above configuration.
Our tsconfig.json file brings in the base configuration and instructs typescript to only include files located within the src
directory for type-checking and compilation to JavaScript. Additionally, it sets the directory to output the compiled JavaScript
to a ./dist/
directory.
4. Configure Prettier
With prettier installed, all we need to do is to add a configuration file at project root. We're going to use a prettier.config.mjs
file (there are a few types of configuration files that prettier offers) as follows:
/*** @see https://prettier.io/docs/en/configuration.html* @type {import("prettier").Config}*/const config = {printWidth: 80,tabWidth: 2,semi: true,singleQuote: false,trailingComma: "es5",bracketSpacing: true,proseWrap: "preserve",};export default config;
This config can be adjusted to suit formatting preferences, these are just some basic choices to get started with.
5. Configure Lint-Staged
Similar to prettier, once installed we just need to add a configuration file which again can be accomplished in a variety of ways but we're going to use a lint-staged.config.mjs
file:
import path from "node:path";const config = {"*.{cjs,mjs,js,ts,jsx,tsx}": (stagedFiles) => [`eslint --fix ${stagedFiles.join(" ")}`,`prettier --write ${stagedFiles.join(" ")}`,],"*.{css,md,mdx,json}": (stagedFiles) => [`prettier --write ${stagedFiles.join(" ")}`,],};export default config;
The above contains two different linting/formatting flows. Firstly for JS and TS filetypes, it uses eslint
to fix any linting errors and then invokes prettier
to format those files. The second configuration is for CSS, Markdown and JSON files and it simply invokes prettier
to format those files.
6. Configure Husky
Finally, let's setup husky which will bring all our work together.
npx husky init
Initialise husky with npx husky init
. This will bootstrap a .husky
directory and add a prepare
script to the project (see the husky docs for a full explanation). Within the .husky
directory you'll see a pre-commit
hook has already been created for you. We're going to replace its contents with that of the tab above.
The git life-cycle has many stages within it and we can execute scripts during those stages by using git hooks. The pre-commit hook is invoked prior the staged files being merged into the current branch. We're adding into the pre-commit hook, the ability to typecheck our repo and run our lint-staged configuration against those currently staged files. The hook will fail if an error is encountered by either command, thereby interrupting the git process, prompting us to address the errors that were flagged in the terminal.
7. Configure eslint
npm init @eslint/config@latest
The final step to take is to install and configure eslint, which we can do with the npm init @eslint/config@latest
command, choosing the following cli options:
✔ How would you like to use ESLint? · problems✔ What type of modules does your project use? · esm✔ Which framework does your project use? · none✔ Does your project use TypeScript? · typescript✔ Where does your code run? · nodeThe config that you've selected requires the following dependencies:eslint, globals, @eslint/js, typescript-eslint✔ Would you like to install them now? · No / Yes✔ Which package manager do you want to use? · npm
When the cli completes, we should now have eslint and its dependencies installed as well as new eslint.config.mjs
file with the selected configuration applied.
That's it, all the setup and configuration is complete. Congrats!
Now What?
Well, all we need to do now is to develop as we normally would! The fruit of our labour should be seen within the terminal when we issue git commit
! Our staged files will be linted and formatted prior to inputting our commit message and all will be well with the world and we'll be rewarded with coffee and chocolate for the rest of our glorious days!
Why not start by adding the following method into the project:
export const main = () => {console.log("Hello, world!");};main();
With this we can test whether compilation to javascript works correctly with npx tsc
and we can also commit the method to test the code quality configuration.
Bypassing Git Hooks...
Git hooks are really powerful and really useful and this setup works super well in almost all instances. However, there may come a time where it might be necessary to perform the unspeakable task of committing work that doesn't pass static analysis or typechecking. In those (reckless but necessary) circumstances, the hook can be bypassed by adding the --no-verify
flag to the git commit
command. E.g.
git commit -m "your commit messsge here" --no-verify"
With great power, comes great yada yada yada. You get it.
Added Extra!
We touched on conventional commits above in the introduction, but as a reminder, it's a concept for standardising commit messages, which in turn makes it easier to understand the unit of work which was added into the branch with each commit. We're going to use the prepare-commit-msg
git hook to invoke the gitmoji
cli which will take over the default terminal experience for adding a commit message, and replave it with an interactive terminal that will guide us through writing a commit message which complies with conventional-commit syntax. But wait, that's not all... This setup will also add emojis to our commit messages, leading to our project earning infinite stars on github, infinite VC investment and more money than even Erlich Bachmann would know what to do with...
npm install -D gitmoji-cli
Let's start with installing gitmoji-cli as a project devDependency. When installed, lets add some configuration to specify how we would like the interactive terminal to behave once it's invoked by adding teh above into the root of our package.json.
Finally lets add a prepare-commit-msg hook with the details above into our .husky directory.
Now when you run git commit
once typchecking, linting and formatting has been completed, we'll be provided an interactive terminal to format our commit message!

If we want to modify the gitmoji-cli configuration, we can checkout the docs here. It's highly recommended to do a bit of background reading into conventional-commit syntax. We're using gitmoji in this example as emojis are objectively awesome... fact. They work great for personal projects, but if team projects require a less delightful way to standardize commit messages, commitizen is a great alternative.
Super Bonus Extra!
For the vscode users reading this, here's some IDE-specific configuration to complement our code quality workflow. Create a .vscode
directory at project root and create each of the following within:
{"recommendations": ["mylesmurphy.prettify-ts","orta.vscode-twoslash-queries","yoavbls.pretty-ts-errors","esbenp.prettier-vscode","dbaeumer.vscode-eslint"]}
- Recommended extensions for projects using eslint, prettier and typescript
- project-scoped settings for invoking the prettier and eslint extensions to do their thing as files are saved.
Thanks for taking the time to read this article. I hope it helps and inspires you to learn and share your knowledge with others!