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:
- A
.env.example
file should be at the root of the project, and it should contain relevant default values for setting up the project. - The
.env
files should never be committed in the repository and added to the.gitignore
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:
- A
docker-compose.yml
file should be present at the root of the project and used to quickly spin-up a local environment with the proper services (Postgres, Redis, Mailhog, etc.). - The
docker-compose.yml
file should refer to environment variables present in the.env
file so we don’t need to duplicate the values. It is a good idea to set default values using the interpolation syntax.
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:
- Prefer using
pnpm
as your package manager for better speed and storage optimization. - Use Prettier and ESLint together, ensuring no stylistic conflicts by using
eslint-config-prettier
. - Enforce consistent formatting with an
.editorconfig
file and automate linting with Husky. - Standardize line endings to
LF
using.gitattributes
and setgit config core.autocrlf
toinput
. - Keep environment variables secure by using
.env
(never commit them) and.env.example
for local setup while considering Vault for advanced secrets management. - Optimize your Docker builds:
- Use multi-stage builds to reduce image size and avoid development dependencies.
- Build one environment-agnostic image for QA, staging, and production while injecting environment variables at runtime.
- Stick to even-numbered, LTS Node.js versions, and consider using Volta to manage Node.js installations.
- Use Docker Compose locally for onboarding and quickly spinning up necessary services.
Conclusion
After all the above, we finally have a complete setup. We will continue in another article to discuss the code.