Web DevelopmentBest Practises for Node JS App Development

best practices nodejs

Introduction

Ensuring best practices for Node.js applications is crucial for building robust, scalable, and secure solutions. At Mobile Reality, we emphasize adhering to industry standards and leveraging the latest tools and techniques to maintain our competitive edge. This article aims to provide an in-depth style guide to the best practices NodeJS development, focusing on key aspects such as configuration, security, testing, and performance optimization.

Node.js has become a popular choice for developing server-side applications due to its asynchronous nature and ability to handle concurrent connections efficiently. However, to harness its full potential, developers must follow best practices for Node.js, ensuring that applications are not only performant but also secure and maintainable.

Importance of Best Practices in Node.JS

Adopting best practices in Node.js is essential for several reasons:

  1. Security: Protecting applications from common vulnerabilities and attacks such as prototype pollution, timing attacks, and HTTP request smuggling is paramount. By following security best practices, such as implementing strong authentication policies, using security linters, and ensuring secure HTTP headers, we can safeguard our applications against potential threats.

  2. Performance: Optimizing Node JS performance involves efficient use of CPU cores, managing dependencies before production, and utilizing static analysis tools. Techniques like cleaning the node_module cache, using npm scripts, and adhering to clean code principles contribute to the overall performance and reliability of the application.

  3. Maintainability: A well-structured project with clear module separation, consistent code style, and proper error handling is easier to maintain and scale. Using TypeScript, ESLint plugins, appropriate Node version, and layering components effectively ensures that the codebase remains clean and manageable over time.

  4. Scalability: Preparing production applications involves thoughtful js project architecture and the use of Docker for consistent deployments. By following generic Docker practices and leveraging npm ci for reliable builds, we can achieve downtime-free deployments and maintain high availability.

  5. Testing and Development Process: Implementing testing practices, including the use of favorite test frameworks and async error handling, ensures that our applications are robust and bug-free. The development process is further streamlined by using TypeScript boilerplates, JS boilerplates, and specific JavaScript environments tailored to our needs.

This guide will cover various aspects of Node framework development, providing insights into the practices and security measures that we follow at Mobile Reality. From configuring environment variables and using the npm registry effectively to handling errors and securing Node.js applications, we will delve into the details that make our development process efficient and secure.

Why Mobile Reality?

At Mobile Reality, we believe that a strong foundation in best practices leads to successful enterprise development with our JS development tech stack. Our commitment to quality and security is reflected in our approach to building applications. By following these best practices, we ensure that our solutions are not only innovative but also resilient and future-proof.

Whether you are a seasoned developer or new to Node.js, this guide will provide valuable insights and practical tips to enhance your development process. Let's dive into the world of Node.js and explore the best practices that make our applications stand out.

Backend Configuration

Effective backend configuration is the cornerstone of building scalable and maintainable Node.js applications. At Mobile Reality, we prioritize a systematic approach to configuring our applications, leveraging environment variables, Docker, and proper dependency management. This section will guide you through our best practices for backend configuration, ensuring a robust and secure foundation for your Node.js projects.

Environment Variables

Managing configuration through environment variables is a best practice in Node.js development. It allows for flexible and secure application setups across different environments.

Defining and Using Environment Variables

  1. Consistency and Security:

    • To maintain consistency, all environment variables should be written in UPPER_CASE.

    • Sensitive information should be stored securely in environment variables to avoid hardcoding secrets in the codebase, reducing the risk of attack exposure.

  2. .env and .env.example Files:

    • Use .env as the main file for defining environment variables.

    • Create a .env.example file to document all allowed and required variables. This file serves as a template for setting up the development and CI testing environment.

    • Example of .env.example:

      DB_HOST=localhost
      DB_PORT=5432
      DB_USER=root
      DB_PASS=secret

  3. Mapping and Validating Environment Variables:

    • Each environment variable should be mapped and validated in scripts located in the ./src/config directory. This ensures all necessary configurations are available and correctly formatted before the application starts.

  4. Setting Up the Development Environment:

    • To set up the development environment quickly, copy the .env.example file to .env:

      cp .env.example .env

    • This command simplifies the initial setup and ensures that all required variables are defined.

      cp .env.example .env

Dependency Management

Proper dependency management is crucial to maintain security and performance in Node.js applications.

  1. Handling Dependencies Before Production:

    • Ensure all dependencies are up to date and free from vulnerabilities using tools like npm audit and npm outdated.

    • Regularly clean the node_modules cache to avoid potential issues with stale dependencies:

      npm cache clean --force

  2. Avoiding Vulnerable Dependencies:

    • Monitor and update dependencies to mitigate risks associated with vulnerable packages.

    • Use security tools and linters to identify and address potential security vulnerabilities in your dependencies.

By following these best practices for backend configuration, you can ensure that your Node.js applications are secure, performant, and maintainable. Properly managed environment variables, efficient Docker integration, and vigilant dependency management form the backbone of reliable and scalable enterprise development at Mobile Reality.

Docker Integration

Docker has revolutionized the way we deploy and manage applications, providing a consistent environment across different stages of development and production. At Mobile Reality, we leverage Docker to ensure our Node.js applications are easily deployable, scalable, and secure. This section will guide you through our best practices for Docker integration in Node.js development, focusing on creating efficient Dockerfiles, managing dependencies, and using Docker Compose for seamless service orchestration.

Why Use Docker for Deployment

Docker provides isolated environments that help eliminate inconsistencies between development, testing, and production. This ensures that our applications run smoothly regardless of where they are deployed. Using Docker also simplifies the management of dependencies and external services, which is crucial for enterprise development.

Creating an Effective Dockerfile

An efficient Dockerfile is key to building lightweight and secure images. At Mobile Reality, we use a multi-stage build process to ensure our Docker images contain only the necessary components for running the application in production.

Two-Step Build Process

  1. Stage 1: Build:

    • The first stage focuses on installing dependencies and building the application. This stage includes all necessary tools and npm libraries for building the project.

    • Example of the build stage:

      FROM node:18 as build
      WORKDIR /app
      COPY package*.json ./
      RUN npm ci
      COPY . .
      RUN npm run build

  2. Stage 2: Production:

    • The second stage creates a clean, minimal image with only the required runtime dependencies.

    • This approach reduces the image size and attack surface, enhancing security and performance.

    • Example of the production stage:

      FROM node:18
      WORKDIR /app
      COPY --from=build /app .
      RUN npm ci --only=production
      CMD ["npm", "start"]

Installing Dependencies Correctly

Managing dependencies correctly is crucial for maintaining a secure and performant application.

  1. Using npm ci:

    • npm ci installs dependencies based on the package-lock.json file, ensuring a consistent and reproducible build process.

    • This command is faster and more reliable than npm install for CI/CD pipelines.

  2. Differentiating Dependencies:

    • Use npm i package for production dependencies, such as core libraries used in services.

    • Use npm i -D package for development dependencies, such as libraries for testing and static analysis tools.

Using Docker Compose for External Services

Many Node.js applications depend on external services like databases, caching servers, and message brokers. Docker Compose simplifies the orchestration and management of these services.

  1. Setting Up Docker Compose:

    • Define all necessary services in a docker-compose.yml file.

    • This setup allows for easy startup and consistent configuration across different environments.

  2. Example of docker-compose.yml:

    version: '3.8'
    services:
      db:
        image: postgres:13
        environment:
          POSTGRES_USER: root
          POSTGRES_PASSWORD: secret
          POSTGRES_DB: myapp
        ports:
          - "5432:5432"
      app:
        build: .
        environment:
          - DB_HOST=db
        ports:
          - "3000:3000"
        depends_on:
          - db

  3. Starting Backend Services:

    • Use Docker Compose to start all required services with a single command:

      docker-compose up    # For Docker Compose v1.x.x
      docker compose up    # For Docker Compose v2.x.x

    • Docker Compose loads the .env file, ensuring all environment variables are sourced correctly.

Managing Dependencies Before Production

  1. Cleaning node_modules Cache:

    • Regularly clean the node_modules cache to avoid issues with stale dependencies and ensure that the latest versions are installed:

      npm cache clean --force

  2. Using Security Tools:

    • Monitor dependencies for vulnerabilities using tools like npm audit.

    • Address any identified vulnerabilities to maintain the security and integrity of the application.

Avoiding Vulnerable Dependencies

  1. Regular Updates:

    • Keep dependencies up to date to avoid security risks associated with outdated packages.

    • Regularly check for updates using npm outdated.

  2. Security Linters:

    • Use security linters to identify and fix potential security issues in the codebase and dependencies.

Docker and Node.js Performance

Optimizing the performance of Node.js applications within Docker involves efficient resource management and configuration.

  1. Utilizing CPU Cores:

    • Configure Node.js to utilize available CPU cores effectively, enhancing performance for concurrent requests.

  2. Running in Production Mode:

    • Ensure the application runs in production mode within the Docker container to optimize performance and resource usage:

      NODE_ENV=production  npm start

By following these best practices for Docker integration, we can ensure that our Node.js applications are secure, performant, and maintainable. Docker provides a consistent and isolated environment, making it easier to manage dependencies, scale services, and deploy applications seamlessly. At Mobile Reality, our commitment to these practices ensures that we deliver robust and reliable solutions to our clients.

Development Setup

Setting up a robust development environment is critical for ensuring productivity, consistency, and quality in Node.js projects. At Mobile Reality, we follow best practices to streamline the development process, from installing dependencies to running the application in development mode. This section will guide you through our development setup, focusing on key practices and tools to enhance your workflow.

Installing Dependencies

Proper dependency management is crucial for maintaining a secure and stable development environment. We use npm to handle all dependencies, ensuring that our Node.js applications have all the necessary libraries and tools.

  1. Using npm i:

    • Install all dependencies specified in the package.json file by running:

      npm i

    • This command fetches and installs the required packages, preparing the development environment.

  2. Cleaning the node_modules Cache:

    • To avoid potential issues with stale dependencies, regularly clean the node_modules cache:

      npm cache clean --force

  3. Managing Vulnerable Dependencies:

    • Use npm audit to identify and fix security vulnerabilities in the installed packages. Keeping dependencies up-to-date helps in mitigating risks associated with vulnerable dependencies.

Running the Backend in Development Mode

Running the backend in development mode enables live-reloading and debugging capabilities, facilitating a smoother development process.

  1. Starting the Application:

    • Use the npm run dev command to start the backend in development mode. This command typically uses a tool like nodemon to watch for file changes and restart the server automatically:

      npm run dev

  2. Environment Configuration:

    • Ensure that all environment variables are properly configured and loaded from the .env file. This setup is crucial for mimicking the production environment while developing locally.

Using npm Scripts

npm scripts simplify the execution of common tasks and commands, making the development process more efficient.

  1. Common npm Scripts:

    • Define scripts in the package.json file to automate tasks like starting the server, running tests, and building the project. Examples include:

      {
        "scripts": {
          "start": "node server.js",
          "dev": "nodemon server.js",
          "test": "jest",
          "build": "webpack --config webpack.config.js"
        }
      }

  2. Running Scripts:

    • Execute npm scripts using the npm run command followed by the script name:

      npm run test

Using Static Analysis Tools

Static analysis tools help maintain code quality by identifying potential issues and enforcing coding standards.

  1. ESLint and Prettier:

    • Use ESLint for linting and Prettier for code formatting. These tools can be configured to run automatically on every commit using husky and lint-staged.

    • Example configuration in package.json:

      {
        "husky": {
          "hooks": {
            "pre-commit": "lint-staged"
          }
        },
        "lint-staged": {
          "*.js": ["eslint --fix", "prettier --write"]
        }
      }

  2. Running Linters:

    • Use the following commands to run ESLint and Prettier:

      npm run lint
      npm run lint:fix

Using TypeScript

TypeScript enhances JavaScript by adding static types, which help catch errors early and improve code maintainability.

  1. Setting Up TypeScript:

    • Install TypeScript and configure it in the tsconfig.json file. Ensure TypeScript is used throughout the project for type safety and better development experience.

      npm install typescript --save-dev

  2. Using TypeScript Boilerplates:

    • Start new projects with TypeScript boilerplates to ensure a consistent and scalable JS project structure. This practice helps in maintaining code quality and enforcing standards across the codebase.

Dependencies Before Production Release

Ensuring that all dependencies are properly managed before deploying the application to production is crucial for security and performance.

  1. Auditing Dependencies:

    • Regularly audit dependencies for vulnerabilities using npm audit and update them accordingly.

      npm audit

  2. Optimizing Dependencies:

    • Use npm ci for installing dependencies in CI/CD pipelines to ensure a clean slate and reproducible builds.

      npm ci

Using Docker for Development

Docker can be used to create consistent development environments, mirroring the production setup.

  1. Setting Up Docker:

    • Use Docker Compose to set up the development environment with all necessary services like databases and message queues. This ensures that the local development environment closely resembles the production environment.

      docker-compose up

  2. Running the Application in Docker:

    • Use Docker to run the Node.js application, ensuring that all dependencies and configurations are properly managed.

Following these best practices for development setup ensures a smooth, efficient, and secure development process. By leveraging npm scripts, static analysis tools, TypeScript, and Docker, we can maintain high code quality and streamline the development workflow at Mobile Reality. These practices are essential for building robust Node.js applications that are easy to maintain and scale.

Database Management

Effective database management is crucial for ensuring data integrity, performance, and scalability in Node.js applications. At Mobile Reality, we employ best practices for database configuration, schema design, and migration management using MikroORM. This section will guide you through our approach to database management, focusing on SQL naming conventions, configuration, and handling migrations.

Introduction to MikroORM

MikroORM is a powerful TypeScript ORM for Node.js that provides a high-level abstraction for database interactions. It supports various SQL databases and offers features like entity management, migrations, and a flexible query builder. MikroORM aligns well with our enterprise development goals by ensuring a consistent and type-safe approach to database operations.

Configuring MikroORM with TypeScript

Proper configuration of MikroORM is essential for leveraging its full capabilities.

  1. Configuration File:

    • Place the MikroORM configuration in a TypeScript file (./src/mikro-orm.config.ts). This file should use environment variables mapped in the config directory to ensure flexibility and security.

    • Example configuration:

      import { MikroORM } from '@mikro-orm/core';
      import { config } from './config';
      
      export default {
        entities: ['./dist/entities'], // path to our JS entities (dist), relative to `baseDir`
        entitiesTs: ['./src/entities'], // path to our TS entities (src), relative to `baseDir`
        dbName: config.DB_NAME,
        type: 'postgresql',
        user: config.DB_USER,
        password: config.DB_PASS,
        debug: config.DB_DEBUG,
      } as Parameters<typeof MikroORM.init>[0];

SQL Naming Conventions

Adhering to SQL naming conventions ensures consistency and readability across the database schema.

  1. Table Names:

    • Use plural form for table names (e.g., users, user_comments) to reflect collections of entities.

    • Example:

      @Entity()
      export class User {
        @PrimaryKey()
        id!: number;
      
        @Property()
        email!: string;
      
        @Property()
        createdAt = new Date();
      }

  2. Column Names:

    • Use singular form for column names (e.g., email, is_blocked, created_at).

    • Employ snake_case for all database identifiers to maintain compatibility with SQL's case insensitivity.

    • Example:

      @Entity()
      export class User {
        @PrimaryKey()
        id!: number;
      
        @Property({ fieldName: 'email' })
        email!: string;
      
        @Property({ fieldName: 'is_blocked' })
        isBlocked!: boolean;
      
        @Property({ fieldName: 'created_at' })
        createdAt = new Date();
      }

Managing Database Migrations

Migrations are essential for evolving the database schema safely and predictably. MikroORM provides a robust CLI for managing migrations.

  1. Creating and Running Migrations:

    • Use MikroORM CLI to generate and run migration files. This ensures that schema changes are versioned and applied consistently across environments.

    • Example commands:

      npm run db -- migration:create    # Generate a new migration file
      npm run db -- migration:up        # Apply all pending migrations

  2. Database Management Scripts:

    • Define scripts in package.json for common database operations, providing a convenient interface for managing the database during development.

    • Example:

      {
        "scripts": {
          "db:migrate": "mikro-orm migration:up",
          "db:create-migration": "mikro-orm migration:create"
        }
      }

  3. Handling Schema Changes:

    • Always generate migration files after making changes to entities to keep the database schema in sync with the application code.

    • Use descriptive names for migration files to indicate the nature of the changes.

Example Commands for Database Management

Having a set of predefined commands makes it easier to manage database operations during development.

  1. Listing Available Commands:

    • To list all available MikroORM CLI commands:

      npm run db -- -h

  2. Generating Migration Files:

    • To create a migration file based on changes in entities:

      npm run db -- migration:create

  3. Applying Migrations:

    • To run all pending migrations and update the database schema:

      npm run db

Applying Migration

Applying migrations is a critical step in synchronizing the database schema with the application's data model changes.

  1. Running Pending Migrations:

    • Use the following command to apply all pending migrations:

      npm run db -- migration:up

  2. Reverting Migrations:

    • If needed, you can revert the last migration applied using:

      npm run db -- migration:down

  3. Comprehensive Migration Management:

    • Maintain a thorough migration history to track schema changes over time. This practice helps in troubleshooting and ensures a consistent state across different environments.

Using TypeScript for Enhanced Development

Leveraging TypeScript in database management provides several advantages, including type safety, enhanced code readability, and reduced runtime errors.

  1. Type Safety: Ensure all database entities and operations are type-checked, reducing the likelihood of runtime errors and improving overall code reliability.

  2. Consistent Code Style: Maintain a consistent code style across the project by using TypeScript and ESLint together. This practice enforces a uniform codebase, making it easier to maintain and scale.

Security Considerations

Securing the database is paramount to protect sensitive data and prevent unauthorized access.

  1. Strong Authentication Policies: Implement strong authentication mechanisms for database access, including the use of environment variables to store credentials securely.

  2. Avoiding Vulnerable Dependencies: Regularly audit database-related dependencies for vulnerabilities using tools like npm audit. Ensure that all packages are up to date and free from known security issues.

  3. Mitigating SQL Injection Attacks: Use ORM features like parameterized queries to prevent SQL injection attacks. MikroORM automatically escapes input data, reducing the risk of such vulnerabilities.

Example Database Management Commands

Here are some example commands for common database management tasks, encapsulating the practices mentioned:

  1. Listing Available Commands:

    • To list all available MikroORM CLI commands:

      npm run db -- -h

  2. Generating a Migration File:

    • To create a new migration file based on changes in the entity definitions:

      npm run db -- migration:create

  3. Applying Migrations:

    • To apply all pending migrations and update the database schema:

      npm run db -- migration:up

  4. Reverting the Last Migration:

    • To revert the last applied migration:

      npm run db -- migration:down

Effective database management is critical for maintaining the integrity, performance, and scalability of Node.js applications. By following these best practices, including using MikroORM, adhering to SQL naming conventions, and implementing robust migration strategies, we ensure that our applications are reliable and secure. At Mobile Reality, our commitment to these practices underpins our ability to deliver high-quality solutions that meet the demands of enterprise development.

Code Quality, Style, and Naming Conventions

Importance of Code Quality

Maintaining high code quality ensures that the application is reliable, maintainable, and scalable. It also makes it easier for multiple developers to work on the same codebase without introducing conflicts.

Using ESLint and Prettier

  1. ESLint for Linting:

    • Use ESLint to enforce coding standards and identify potential issues in the codebase.

    • Configure ESLint in the project’s package.json or .eslintrc.js file.

  2. Prettier for Formatting:

    • Integrate Prettier with ESLint to ensure consistent code formatting.

    • Example configuration:

      {
        "extends": ["eslint:recommended", "plugin:prettier/recommended"]
      }

Enforcing Code Style with Husky and Lint-Staged

  1. Husky for Git Hooks:

    • Use Husky to run linting and tests before commits and pushes.

    • Example setup in package.json:

      { "husky": { "hooks": { "pre-commit": "lint-staged" } } }

  2. Lint-Staged for Staged Files:

    • Configure lint-staged to run ESLint and Prettier on staged files.

    • Example configuration:

      {
        "lint-staged": {
          "*.js": ["eslint --fix", "prettier --write"]
        }
      }

Running Linters and Fixing Errors

  • Manual Linting:

    • Run linter manually with: npm run lint

    • Auto-fix linting errors with: npm run lint:fix

Naming Conventions

  1. Directory and File Naming:

    • Use kebab-case for naming directories and files to ensure consistency and avoid issues with case sensitivity across different operating systems.

  2. Example:

    • Directories: user-services

    • Files: user-controller.js

Project Architecture

Vertical Slice Architecture

Organizing code by features rather than technical layers helps in managing complexity and improving maintainability.

General Project Structure

  1. Directory Layout:

    backend
    ├── src
    │   ├── common         // Shared data across Node modules
    │   ├── config         // Environment variable mapping and validation
    │   ├── migrations     // Database migration files
    │   ├── modules        // Core application parts
    │   ├── scripts        // CLI scripts
    │   ├── test-utils     // Test utility functions
    │   └── main.ts        // Server setup and startup
    ├── .env.example       // Environment variables for development and CI testing
    ├── Dockerfile
    ├── docker-compose.yml
    ├── jest.config.js
    ├── package.json
    ├── tsconfig.json

Modules Structure

  1. GraphQL Modules:

    • The directory structure for GraphQL-based projects:

      modules
      └── cats
         ├── entities        // MikroORM entities
         ├── loaders         // Data loaders to avoid GraphQL N+1 problem
         ├── models          // GraphQL models
         ├── mutations       // GraphQL mutations
         ├── queries         // GraphQL queries
         ├── services        // Shared logic
         └── cats.module.ts  // Module definition

  2. REST Modules:

    • The directory structure for REST-based projects:

      modules
      └── cats
         ├── dtos            // Data transfer objects
         ├── endpoints       // REST endpoints
         ├── entities        // MikroORM entities
         ├── services        // Shared logic
         └── cats.module.ts  // Module definition

Testing Best Practices

Importance of Testing

Thorough testing ensures that the application behaves as expected and helps in identifying bugs early in the development process.

End-to-End Testing

  1. Effective E2E Tests:

    • Write comprehensive end-to-end tests to cover all user interactions and expected outcomes.

  2. Running E2E Tests:

    • Use the following command to run E2E tests:

      npm run test:e2e

Conclusion

In conclusion, adhering to best practices in Node.js development is crucial for building robust, scalable, and secure applications. At Mobile Reality, we prioritize security by implementing strong authentication policies, avoiding vulnerable dependencies, and using security linters to protect our applications from potential threats. Optimizing performance is another key focus, achieved through efficient dependency management, utilizing Docker for consistent environments, and leveraging TypeScript for type safety and enhanced code quality.

Maintaining a high level of code quality and consistency is essential for the long-term success of our projects. We enforce strict code style guidelines using tools like ESLint and Prettier, and we ensure that our codebase remains clean and manageable by following naming conventions and organizing our code according to vertical slice architecture. This approach not only improves the readability and maintainability of our code but also makes it easier to onboard new developers and collaborate effectively.

By integrating these best practices into our development workflow, we create a foundation for delivering high-quality software that meets the demands of enterprise development. Our commitment to these practices enables us to produce applications that are reliable, secure, and efficient, ultimately providing greater value to our clients and users. As we continue to evolve and adapt to new technologies and methodologies, these best practices will remain at the core of our development process, guiding us toward even greater achievements in the future.

Updated at19.06.2024
Published at18.06.2024
Oskar Skowroński
Oskar Skowroński

Backend and NodeJS Developer

Bartłomiej Lipiec
Bartłomiej Lipiec

Backend and NodeJS Specialist

Table of contents

  1. Conclusion

Share the article

Did you like the article?Find out how we can help you.

Matt Sadowski

CEO of Mobile Reality

CEO of Mobile Reality

Related articles

Discover essential React JS Best Practices for Frontend Development Teams. Elevate your coding game with expert tips and techniques.

22.04.2024

Top ReactJS Best Practices for Frontend Teams

Discover essential React JS Best Practices for Frontend Development Teams. Elevate your coding game with expert tips and techniques.

Read full article

Discover the essential guide for CTOs comparing GO vs Node JS. Make the right choice for your tech stack. Get insights now! #node #nodejs #go #golang #CTO

25.04.2024

GO vs Node JS : A Complete Comparison for CTOs

Discover the essential guide for CTOs comparing GO vs Node JS. Make the right choice for your tech stack. Get insights now! #node #nodejs #go #golang #CTO

Read full article

Discover the essential guide for CTOs comparing Node JS vs PHP. Make the right choice for your tech stack. Get insights now! #nodejs #php #CTO

14.06.2024

Node JS vs PHP: A Complete Comparison for CTOs

Discover the essential guide for CTOs comparing Node JS vs PHP. Make the right choice for your tech stack. Get insights now! #nodejs #php #CTO

Read full article