Overview

Whenever I start a new project, one of the first things I do is put in place a code linter. For the uninitiated, linters analyze your project and call out potential issues. They can provide guidance on everything from how much spacing is used between lines to specific implementation details, such as restricting the usage of certain patterns and habits. You can take these tools one step further by making them part of your CICD workflow, so your build fails if it detects non-compliant code.

IDE showing a linting error
Gotta keep it tidy yo

To many, this may seem like a hassle, but depending on the scale of the project and the number of people working on it, using these tools is a way to standardize shared best practices and opinions across a whole team. After all, everyone is opinionated when it comes to writing code. You can take two completely different code bases written in the same language, and neither would look the same. In JavaScript, there are multiple ways to write the same thing. There are various ways of writing loops, defining functions, and even variables. As more people work on the same code base, their opinions come with them, and without a way to standardize, you'll soon end up in a hellhole of pull requests where people constantly leave unproductive "/nitpick" comments for basic things. 

Linting and Formatting JavaScript

Setting up these types of tools is typically straightforward; you'll install a package, often from a registry, and then run something either on the command line or directly within your IDE with the help of a plugin.

One prevalent linting option for JavaScript is ESLint. ESLint is based on a shared configuration file you provide in your repository. Let's look at a relatively simple example of a configuration that inherits a series of grouped recommended rules for best practices. The community drives these recommended rules and even provides a mechanism for auto-fixing some of them with the –-fix flag.

eslint.config.js
import js from '@eslint/js';
import globals from 'globals';

export default [
  js.configs.recommended,
  {
    languageOptions: {
      ecmaVersion: 12,
      globals: {
        ...globals.browser,
      },
    },
  }
];

In addition to using the recommended rules, I also like to expand on them by adding several optional, more opinionated rules. For example, I'm not too fond of functions with many parameters as I've found that in the past, they can cause problems and generally become hard to follow, so in most of the codebases I work on, I enforce this using the max-params rule. I also like to ensure that my code is formatted a certain way, so I use the Prettier plugin to ensure that everything matches my Prettier config file, preventing discrepancies around commas and spacing.

eslint.config.js
import js from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';

export default [
  js.configs.recommended,
  eslintPluginPrettierRecommended,
  {
    languageOptions: {
      ecmaVersion: 12,
      globals: {
        ...globals.browser,
      },
    },
  },
  {
    rules: {
      'max-params': ['error', 3],
      'no-unused-vars': 'error',
    },
  },
];

With this configuration, the following function will flag an error directly in my IDE or when I run ESLint via the command line, as it does not adhere to my max-param restrictions or the prettier configuration. If I were to run the linter in my CICD workflow, it would cause the build to fail, preventing me from merging it upstream, which is crucial if we want to ensure our shared branches are always in a good state.

fetch.js
const fetchData = async (endpoint, token, headers, params) => {
  try {
          const response = await fetch(endpoint, {
      headers: Object.assign({
        Authorization: `Bearer ${token}`
      }, headers),
      params
    });
    
const data = await response.json();

    
const newData = {
      ...data,
    additionalProperty: 'newValue',
    };

                return newData;
  } catch (error) {
    console.error('Error fetching data:', error);
  }
};

fetchData('https://api.example.com', 'token', { 'Content-Type': 'application/json' }, { page: 1 });
$terminal
/Users/jives/fetch.js
   1:60  error  Async arrow function has too many parameters (4). Maximum allowed is 3                                                                                                                                                                  max-params
   2:3   error  Delete `··`                                                                                                                                                                                                                             prettier/prettier
   2:11  error  'cat' is assigned a value but never used                                                                                                                                                                                                no-unused-vars
   3:3   error  Delete `··`                                                                                                                                                                                                                             prettier/prettier
   4:1   error  Delete `········`                                                                                                                                                                                                                       prettier/prettier
   5:1   error  Replace `········headers:·Object.assign(` with `······headers:·Object.assign(⏎········`                                                                                                                                                 prettier/prettier
   6:43  error  Insert `,`                                                                                                                                                                                                                              prettier/prettier
   7:11  error  Replace `·headers)` with `⏎········headers`                                                                                                                                                                                             prettier/prettier
   8:7   error  Replace `··params` with `),⏎······params,`                                                                                                                                                                                              prettier/prettier
   9:1   error  Replace `······` with `····`                                                                                                                                                                                                            prettier/prettier
  10:1   error  Replace `······⏎` with `⏎··`                                                                                                                                                                                                            prettier/prettier
  12:1   error  Delete `··`                                                                                                                                                                                                                             prettier/prettier
  13:5   error  Delete `··⏎······`                                                                                                                                                                                                                      prettier/prettier
  15:7   error  Replace `········additionalProperty:·'newValue'` with `additionalProperty:·'newValue',`                                                                                                                                                 prettier/prettier
  16:1   error  Replace `······` with `····`                                                                                                                                                                                                            prettier/prettier
  17:1   error  Delete `··`                                                                                                                                                                                                                             prettier/prettier
  18:5   error  Delete `··············`                                                                                                                                                                                                                 prettier/prettier
  19:1   error  Replace `····` with `··`                                                                                                                                                                                                                prettier/prettier
  20:1   error  Delete `··`                                                                                                                                                                                                                             prettier/prettier
  21:3   error  Delete `··`                                                                                                                                                                                                                             prettier/prettier
  22:1   error  Delete `··`                                                                                                                                                                                                                             prettier/prettier
  23:1   error  Delete `··`                                                                                                                                                                                                                             prettier/prettier
  24:1   error  Replace `··fetchData('https://api.example.com''token'{·'Content-Type''application/json'·}{·page:·1·});` with `fetchData(⏎··'https://api.example.com',⏎··'token',⏎··{·'Content-Type''application/json'·},⏎··{·page:·1·},⏎);`  prettier/prettier

23 problems (23 errors, 0 warnings)

If I resolve these issues, I no longer get an error, and the build will pass.

fetch.js
const fetchData = async ({ endpoint, token, headers, params }) => {
  try {
    const response = await fetch(endpoint, {
      headers: {
        Authorization: `Bearer ${token}`,
        ...headers,
      },
      params,
    });

    const data = await response.json();

    const newData = {
      ...data,
      additionalProperty: 'newValue',
    };

    return newData;
  } catch (error) {
    console.error('Error fetching data:', error);
  }
};

fetchData({
  endpoint: 'https://api.example.com',
  token: 'token',
  headers: { 'Content-Type': 'application/json' },
  params: { page: 1 },
});

Linters can also be a great way to reinforce good habits. For example, having a linting rule in your project for no unused variables can be a great way to teach the practice of not leaving unused references in code. As a maintainer, it improves your quality of life, and it's one less thing you'll need to consider during the pull request process. You can even use a linter to enforce that any types aren't used in TypeScript and other harmful practices that can cause bugs in your code.

Over time, your configuration file will evolve as you form best practices and needs amongst your team; you may decide to add or even remove unnecessary rules. The ultimate goal is more approachable code and less wasted time—what's not to like?

You Can and Should Lint CSS

You can even lint your stylesheets if you're working with CSS. One of my favourite tools for that is Stylelint. Similar to ESLint, it's configuration-based and lets you define what rules you want to include, it also has a recommended configuration that you can extend from.

.stylelintrc.json
{
  "extends": "stylelint-config-standard"
}

For example, linting CSS can be beneficial in cases where you need to support legacy browsers. Downgrading JavaScript is pretty common, but it's not always as simple for CSS. Using a linter allows you to be honest with yourself by flagging problematic lines that won't work in older environments, ensuring your pages look as good as possible for everyone.

.stylelintrc.json
{
  "extends": "stylelint-config-recommended",
  "plugins": [
    "stylelint-no-unsupported-browser-features"
  ]
  "rules": {
    "plugin/no-unsupported-browser-features": [true, {
      "browsers": ["Chrome >= 66"]
    }]
  }
}

Using this Stylelint plugin, the following CSS would flag an error as you can't use flex-gap in older Chrome versions. These properties are commonplace in modern codebases and can be easily missed if you're not testing older browsers. You can catch these issues with a linter before they become a problem.

styles.css
.container {
  align-items: center;
  display: flex;
  gap: 10px;
  justify-content: center;
}
$terminal
[js] src/styles.css
[js]    4:1  ✖  Unexpected browser feature "flexbox-gap" is not supported by Chrome 66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,83  plugin/no-unsupported-browser-features

CSS linting can also be leveraged to ensure that you stick to a single unit of measurement for things like font sizes and even enforce the usage of CSS variables for certain property types, like colours and padding. These rules go a long way to providing consistency, especially when you may have multiple views or components.

Lint Early, Lint Often

Creating a baseline set of standards around linting at the start of a project is much easier. Adapting code as you write is much more straightforward than retrofitting an existing project. If you're starting a new project and already have another application that uses a linter, add the same ruleset to your new one and adapt it as you go along. Just because it's harder to adapt a linter to something already established doesn't make it any less worth it, though; it's just more of a time sink.

Most linters allow you to mark a rule as a warning or an error; warnings inform you of the problem when the linter is run, whereas an error will break your build pipeline, preventing it from going any further. I typically avoid setting any rules as a warning unless I am actively migrating code to where I can turn it on as an error. My process is usually:

  1. Determine what rules should be enabled, discuss with your team if you're not working solo.
  2. Enable the rule as a warning.
  3. Fix the code that is flagging the warning.
  4. Enable the rule as an error once all the warnings are resolved.
  5. Avoid lint-ignore comments unless absolutely necessary.

I've made the mistake of doing large refactors to turn on linting rules and broken things. Play it safe and slowly make changes over time to avoid unnecessary headaches.

Jives Says

Closing Notes

Linting is a great way to ensure that your codebase is consistent and that you're following best practices; many options are available for different languages and frameworks that you can find with a quick search. They can be a pain to set up for established projects, but the benefits far outweigh the initial setup time. It's also a great way to reinforce good habits and ensure your codebase is clean and maintainable. If you're not already using a linter, I highly recommend you start. It's a small change that can positively impact your quality of life as a developer.