Maintain a high quality codebase with an ease

willaiem

Damian Żygadło

Posted on March 6, 2023

Maintain a high quality codebase with an ease

Some tools exist to help developers keep the code consistent and easier to understand.

I want to show you various tools to help you write better quality software.

Note: this article was created with web developers in mind, but non-web folks can also find some exciting things.

Table of Contents

  1. Extensions
  2. Tools
  3. Tests (units, integrations and E2E)
  4. Principles

Extensions

Extensions

Those are the easiest ones to set up. The huge advantage is that they're global, so you can use them everywhere without installing them in the project.

These are the ones that I use (and I can freely recommend them):

SonarLint

A great tool that gives you instant feedback based on your code.
Can suggest some improvements and catch potential bugs.

Abracadabra, refactor this!

Enhances the refactoring options in VS Code, which makes the refactoring code easier.
This won't automatically make your code better, but it is a helpful tool to have.

JS Refactoring Assistant (paid)

This is a combination of the previous two extensions.
It suggests refactoring changes to make the code cleaner and easier to read.

Tools

Tools

ESLint

A must-have utility for any JavaScript project. It helps to keep the code style consistent, and it can prevent different anti-patterns and common mistakes.

To make work with ESLint easier, there's the ESLint extension for VS Code, which includes the commands to check the logs and restart the server, which makes it easier to debug the configs.

My personal config files

  • .eslintrc.js
module.exports = {
  'env': {
    'browser': true,
    'es2022': true,
    'node': true
  },
  'extends': [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/strict',
    'plugin:react/jsx-runtime',
    'eslint-config-async'
  ],
  'overrides': [
  ],
  'parser': '@typescript-eslint/parser',
  'parserOptions': {
    sourceType: 'module',
    tsconfigRootDir: __dirname,
    project: ['./tsconfig.json'],
  },
  'plugins': [
    'react',
    '@typescript-eslint'
  ],
  'rules': {
    'quotes': [
      'warn',
      'single'
    ],
    'semi': [
      'warn',
      'never'
    ],
    '@typescript-eslint/no-floating-promises': 'error',
    '@typescript-eslint/no-unsafe-call': 'warn',
    '@typescript-eslint/no-unsafe-return': 'warn',
    '@typescript-eslint/no-unnecessary-type-assertion': 'warn',
    'no-shadow': 'error',
    'prefer-const': 'warn',
    'no-console': 'warn',
    'no-debugger': 'warn',
    'no-magic-numbers': ['error', { ignore: [1, 0, 404, 200, 500] }],
    'no-dupe-else-if': 'error',
    'max-depth': ['error', 4],
    'max-lines': 'warn',
    'max-params': ['error', 3],
    'no-unneeded-ternary': 'error',
    'react/boolean-prop-naming': 'error',
    'react/jsx-max-depth': ['warn', { max: 5 }],
    'import/no-default-export': 'error',
  }
}

Enter fullscreen mode Exit fullscreen mode

I'll describe the most important rule I decided to use:

  • no-floating-promises - enforces you to handle the promise. I had multiple situations that I forgot to use await which led to bugs and false positives in my tests.
  • no-unsafe-call - disallows calling any value that is typed as any.
  • no-unsafe-return - prevents from returning the any type from the function.

You can read about the rules here.

tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "types": ["node", "jest", "@testing-library/jest-dom"]
  },
  "include": [
    "src",
    ".eslintrc.js",
  ],
}
Enter fullscreen mode Exit fullscreen mode

You can adjust it to your needs.

Husky

Pre-commit hooks are (mostly) cool.
Theo - ping.gg wouldn't like this, but I don't care.

Sometimes developers forget to run tests, or you accidentally push some invalid code to the repo.

Husky helps run the hooks (some tasks) that will be run, i.e. whenever you commit or push the changes to the repo.

This forces the validation to run before the code is pushed to the repo.

However, do not run heavy tasks like testing E2E or the whole project cause it will be time-consuming and highly annoying.

My personal recommendation:

  • Pre-commit hooks for commit messages.
  • Pre-push hooks for linters, unit and integration tests.

Commit messages

PLEASE, please, make your commits consistent. This makes the Git history and pulls requests easier to track and potentially rollback the changes.

My go-to choice is Conventional Commits - give it a shot and connect it to Husky via Commitlint.

CI/CD (GitHub Actions, CircleCI, etc.)

They are helpful to automatically deploy your app (for example, to preview the deployment to test it manually) and run all the tests and linters to check whenever the branch is ready to review.

This is the most straightforward workflow file for GitHub Actions to run the tests whenever the pull request is created.

name: pull-request
on:
  pull_request:
    branches: [ main ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v3.3.0
      - name: Set up node 
        uses: actions/setup-node@v3.6.0
      - name: Install dependencies
        run: npm install
      - name: Run tests 
        run: npm test
Enter fullscreen mode Exit fullscreen mode

You can adjust it to your needs.

Tests (units, integrations and E2E)

Tests

Those are the ones that will save your code from regression whenever you decide to refactor the code.

What tests should be like?

  • Avoid the worst assertion in the world - comparing booleans or using methods that need to return more context when the error occurs.

Like expect(condition).toBe(true) or expect(items.length).toBe(5)

  • They should make sense 😮‍💨 (writing tests just for the sake of testing doesn't make sense at all)
  • They don't care about the code implementation
  • Check both the "happy path" and "sad path"
  • Readable, easy to understand
  • Shouldn't slow the development

You can learn about these here: bewebdev.tech - Testing.

Principles

Principles

No tool will save you from writing readable code.

  • Creating reasonable abstractions (i.e. by using Bridge or/and Adapter patterns)
  • Using relevant data structures to the problem you are trying to solve
  • Breaking up the larger part of the code into smaller ones
  • Writing declarative code by using built-in functions
  • Taking care of immutability in your functions
  • Good, consistent naming conventions
  • Writing code that is easy to test
  • And probably many, many things that I probably forgot to mention 😅

All of these will make your code significantly easier to maintain and scale.

You can learn about these topics here: bewebdev.tech - Clean Code and bewebdev.tech - Design Patterns.




bewebdev.tech is a resource hub for web developers that helps them to keep up with industry needs.
💖 💪 🙅 🚩
willaiem
Damian Żygadło

Posted on March 6, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related