Nest.js Convention and Best Practices: Project Setup

26 May 2025 | Eric

This document serves as the foundation for Nest.js best practices. It is not exhaustive and can be completed over time. Also, best practices evolve over time.

If you are extending, enhancing, or bug-fixing already implemented code in a project you just joined, use the style and practices that are already in place instead of what is in this document so that the source is uniform.

Project Setup

Usually, we set up Nest.js in a Nx repository. But for the sake of simplicity, we are only going to discuss Nest.js best practices and not Nx architecture. Another documentation will cover a typical Nx architecture.

Packages Manager

As much as possible, we should use pnpm. PNPM is much faster than NPM and optimized to save storage on your local system. Overall pnpm provides a better developer experience.

Linting & Prettifying

The goal of linting and prettifying is to have consistent coding conventions and static checks enforced so that the syntax used is always the same. This helps with the readability of the code.

Nest.js comes with pretty good defaults to which we will add:

Add "printWidth": 120 in the .prettierrc. This is to allow more than 80 character lines. Nowadays, most developers have big screens. Let’s not limit ourselves to 80 characters.

An .editorconfig file should be at the root and at least enforce:

root = true  
  
[*]    
end_of_line = lf  
insert_final_newline = true  
indent_style = space  
indent_size = 2  
max_line_length = 120  
ij_visual_guides = 120

By default, your editor should be properly configured to run the linting process every time a file is saved. Though, to ensure that this run properly it is a good idea to install Husky and make it run the linter and potentially the automated tests before the code is pushed to the remote repository.

ESLint

First, what’s the difference between ESLint and Prettier?

Prettier is an opinionated code formatter that makes the style of the code consistent. ESLint is a linter whose goal is to check for code quality AND sometimes apply stylistic rules as well.

The issue with using both ESLint and Prettier is they often conflict with each others, therefore, we want to remove all ESLint stylistic rules when using them together.

This should be done already by default is most Nest.js projects. But if your IDE has specific rules and override the one configured in the project, you might run into trouble.

Make sure ESLint stylistic rules are disabled using eslint-plugin-prettier/recommended.

You can read more about this here.

Git

An issue that often arises when working in a team is some people will commit files with a CRLF line ending (looking at you, Windows users) and some with a LF line ending (Mac, Linux, WSL). CRLF is bad, only Windows uses it, and it can be detected as code change in some cases. And in worst case scenarios, CRLF can leak to production servers that are running on Linux and create unexpected bugs. If you ever experienced bugs because you were using backslashes instead of forward slashes, then this is pretty similar.

To unify this, we force everyone to work with LF line ending. We already specified the line ending option in the .editorconfig above, now we create a .gitattributes file at the root of the project:

echo "* text=auto" > .gitattributes
git add .gitattributes
git commit -m 'adding .gitattributes for unified line-ending'

Another important point is to properly configure your local git and set core autocrlf=input. This tells git to never translate the output to CRLF. Basically, the end-of-line characters will remain as they are received.

git config set --global core.autocrlf input

If after doing all that you still have remaining CRLF it often means you or someone in your team hasn’t set their local environment properly and are still pushing the incorrect end of line to git.

Remember that, for this to work properly, you should also set your IDE so that it never uses CRLF.

You can read more about unifying line ending here.

Environment

Environment variables and keys/secrets should never reach your VCS.

If your environment variables ever reach your VCS, you have to immediately remove the file and change all keys/secrets.

Environment variables should be local only as they are environment-dependent. To solve this issue we use environment variables in a local .env file:

Other configurations that use environment-dependent values should be derived from the .env file, like Docker below, or MikroORM later.

In Github/Gitlab these variables can be defined and secured and used in the CI/CD pipeline where the .env file will be rebuilt automatically, obfuscating secrets and sensible variables.

For more advanced usage, we can use Vault as a environment variables and secrets manager.

Docker

Docker is the building block of our applications. It is used to lock the state of an application in an image. This image, or snapshot is done at build time, and later the same image will be used for deploying on different environments.

You should only generate ONE Docker image and use it for QA, Staging, and Production. If you generate a different image at each stage, you have no guarantee that what you are testing will actually work in production.

Consequently, a Docker image needs to be environment agnostic. The environment-dependent values are injected using an environment file, using the --env-file parameter when the image is run, or in a Docker Compose file. This can be done when running the image manually, or via the AWS ECS or other App Services in your favorite cloud provider.

To ease the onboarding of new team member, we use Docker Compose locally, and only locally. It’s very rare to use Docker Compose in a production environment except if you are self-hosting and not using container services like AWS ECS or EKS:

Later, at build time, each application, or in our case our Nest.js application need to be built in a Docker image. The issue here is most of the default Dockerfile creates an image containing all the development dependencies. The result is an image that weights around 1Go or even more.

To avoid this issue, we build using multi-stage builds, and install only the production dependencies. So it’s important to properly separate the devDependencies in the package.json file. The result is an image that weights around 200 or 300MB.

Don’t use Ubuntu as the base image of your Dockerfile. There are lightweight Linux distributions that are more suitable for hosting like: node-slim or node-alpine.

If Nest.js is installed with Nx you can use the setup-docker generator to do all that, but it’s still a good thing to understand what is happening here.

You can read more about this here.

Node.js Version

You should always use the same Node.js version for your local environment and your Dockerfile. First, learn how Node.js releases are organized. You should never use an odd-numbered version (21, 23, …) and instead use the last LTS version, which is even-numbered (22, 24, …).

If you need to set up multiple versions of Node.js on your computer, I recommend you use Volta to manage all your Node.js installations and never worry anymore about using an outdated version.

TL;DR

To set up a Nest.js project efficiently:

Conclusion

After all the above, we finally have a complete setup. We will continue in another article to discuss the code.