Best Code Practices for Scalability, Collaboration, and More
When coding you should always strive to use established best practices, not just because they’re the best thing to do, but for a whole host of reasons like:
- preventing bugs and catching them earlier
- promoting a set standard of code quality to help make projects more maintainable
- helping a team scale without sacrificing time or quality of the work
In this post, we’re going to be looking at eight different best practices you should be using to help your code and projects become more scalable, while also improving collaboration.
Write high-quality code comments
The first item on our list is code comments. I personally love them, but others subscribe to the school of thought that code comments shouldn’t be used because the code itself should be self-explanatory (e.g. functions should be named for what they do, variable names should describe what they contain, and you should abstract complex calculations to their own variable, etc.). Now, I agree that it’s important to make code self-explanatory; it 100% should be. However, I disagree with avoiding code comments altogether because, if used correctly, they provide a great way to share insights, extra info, and nuances that could never be expressed in the code itself.
Let’s be honest: we don’t live in a perfect world, and sometimes less-than-ideal solutions are implemented for a variety of reasons. Code comments allow us to express the thinking and logic behind those solutions, so future developers maintaining the code (which may even be ourselves in a few months or years) can have insights into what we were thinking, which will save them precious time.
Writing good code comments isn’t an exact science but here are a few rules you can follow to ensure your comments help, rather than hinder your team.
- Avoid copying the code. Every line of code doesn’t need a comment. If the purpose of the code is clear enough, then don’t restate it all in a comment just for the sake of adding a comment.
- Comments should solve any confusion, not add to it. When writing your comments, always ask yourself, “Does this comment help the reader understand the code and its purpose?”
- Explain any unclear code that people may question. If the code isn’t clear to the reader and might leave other devs thinking “why has someone done this?” add a comment and explain your thinking. These comments are especially important where less-than-ideal solutions have been implemented on purpose because future devs may want to optimize or change these solutions; your comment allows you to let them know why it was done that way to start with, which gives them more context for optimization.
- Add sources. This is one of the most important rules; if you’ve taken complete code blocks from online sources, add a quick comment to detail where the code came from with a link to it. This simple act will help any future developers get the full story of the code block along with its context, while also giving credit to the original author.
Don’t skip code documentation when it’s needed
Similar to the first section on code comments, code documentation is another way we can help our future selves and other developers (both internal and external) understand our work or project.
Documentation is more fleshed out and often longer than code comments. It’s for developers using a package or API and wondering what a method does. Or, it could be for internal developers covering technical concepts and implementation details for a feature in a project.
Now, we don’t need to write documentation for everything; a simple, well-named function likely doesn’t need docs written, but what about the entire complex logic of a library, package, or product? As the scale of a project or piece of work grows, so does the time and resources required to understand it for a new developer.
This is why developers need to write documentation. I know we all think we can remember everything and don’t need to write docs, but we’re only human so we will forget it, and then we’ll be kicking ourselves for not writing them while everything was fresh. There is little excuse for not doing it nowadays with tools like TSDoc for TypeScript being so prevalent and making inline documentation easy to produce.
Moving on from documentation and onto some more technical best practices, our first stop is testing. There are three kinds of tests you’ll see commonly used in various amounts in projects:
- Unit tests - Testing individual pieces of code in isolation like a function or an individual React component.
- Integration tests - Testing a combination of individual pieces of code like a series of functions calling each other, a group of React components working together, or a component calling a function to do something.
- End-to-end tests (E2E) - This testing most closely resembles a user’s real-world experience of using a product or application. This type of test helps validate if a product is fit for its purpose and ready to use.
Regardless of the type of tests implemented, they all serve the same purpose, which is to catch bugs, errors, and issues before they make their way into production where the end-user might be impacted by them.
Now, it’s pretty clear why tests are important: no one wants their customers to experience issues or bugs that detract from their experience. But, even though tests are extremely valuable, they’re often the last thing added to a product and the first thing to go when time gets a bit tight. However, this short-term mindset can bite you later on if you’re not careful.
Tests are there to stop bugs from making their way into production, but they can only do that if they’re actually written (and written well). So if we skip writing them every time the pressure goes up, don’t we shouldn’t be surprised if we find bugs on old features in production that weren’t there a week ago. So, if you’d rather be spending your time implementing new features and improving your customer’s experience instead of hunting down old bugs for the third time, start writing tests to cover your code and application so the bugs are evident before the code is merged.
All of these reasons are especially true as a team grows. When there are just one or two developers it’s easier to manually check each PR for regressions in the app and possible bugs in the code. But, as the team grows and the number of PRs increase with it, performing the same level of extensive checks becomes harder to near-impossible. This is where having automated tests running on a CI environment can help you and stop you from merging in faulty code.
Create conventional commit messages
Commit messages are a great tool for providing helpful insights into your code changes; they can also be the key to unlocking some powerful tooling. But, they can only do this when they’re created and formatted properly with a helpful description (not just whatever the developer was feeling that day).
For the formatting, the Conventional Commits Spec gives us a great framework for writing consistent and helpful commit messages to ensure every message follows a set pattern that promotes uniformity across the team and repository.
Once, this uniformity has been achieved, some powerful tooling becomes available for us to use. One of my favorites is the semantic-release package. This npm package lets us generate changelogs and calculate versions automatically, as well as publish npm packages all from a CI environment using our commit messages. If you maintain an npm package or a versioned repository, this is one tool to supercharge your workflow and save you time! 🚀
If you’re worried about enforcing this specification and making sure everyone follows it, don’t be. Tools like commitlint can be paired with CI pipelines and Husky to easily lint each commit message as it’s being created or reviewed to ensure it follows the specification precisely. If the commit message doesn’t follow the rules it will be flagged to the user or, in the case of Husky hooks, the commit will be aborted altogether and not created.
Write consistent code
When working in a team, it’s common for everyone to have a preference for how they like to code and have it formatted. This is great for personal projects where it’s just a single developer working but not so much for projects where several developers are collaborating at once. For multi-developer projects, you need everyone singing from the same hymn sheet to make sure the code is written consistently across the application regardless of who wrote it and when. This change helps developers focus on solving problems and not wasting time adjusting to different code formatting rules between files.
But, don’t worry you don’t need to spend countless hours reinforcing a set style and getting people to rewrite their code to meet that style as part of the PR process. There’s some great tooling like ESLint and Prettier that can do all of this for us. All we need to do is agree on a set configuration of rules and get everyone (and our CI) using it; then it’s off to the races, while we let our IDE/terminal handle all of the code formatting for us!
Get comfortable with code and product validation
It’s been a bit of a consistent theme throughout the last few sections but it’s important that before we merge new code, it’s been validated to show it solves the problem it intended to, is error-free, and complies with all of the agreed standards. This is a lot for one person to manually do for each PR review but don’t worry we can leverage CI pipelines and Husky hooks to do it for us.
Using these tools we can ensure each new piece of code is formatted correctly, all tests in the project pass, and our commit message follows the correct specification. In short, if all the required checks pass on new code, we can be confident that it meets our requirements for code quality. So, all we need to do is check that the functionality is correct and perform a code review to check for things like optimizations or potential shortcomings.
Honestly, there is little reason not to use these tools in your project configuration; not using them greatly increases the risk of bugs, errors, and incorrectly formatted code slipping through into production, causing more work for you and your team in the long term.
Some might consider these overkill for smaller projects with fewer developers, especially solo projects, but I would argue on the contrary. Having these tools configured for smaller projects, lets you focus on just making progress while being confident that bugs and bad code aren’t entering production.
There are other benefits as well; if you’re just starting out for example, by practicing these concepts and technologies on solo projects you’ll make yourself a better team member for future projects. Also, just because you’re on a small team or running a small product now doesn’t mean it always will be that way. Think of the future and get scalable tooling configured at the beginning.
Prioritize scalability and reusability
When coding, two concepts should always be at the forefront of your mind: scalability and reusability. As soon as you start repeating code, you should be asking if the code can be optimized to allow for it to be better reused. For example, if you were repeatedly fetching data using the fetch API, instead of writing out the config and error handling each time for similar calls, make a function that takes in your endpoint and data to minimize duplication.
Even beyond the code itself, scalability and reusability should even be worked into our project setup and configuration. At their core, these two principles are all about how we can set up our code and project now to make our and others’ lives easier tomorrow.
More to explore:
Processes, standards, and principles
One of the worst things that can happen in a team is having members pull in opposite directions; some want to do this convention, others another, and it just ends up in one big mess. What’s needed to avoid this and ensure uniformity and consistency across the project is an agreed-upon set of processes and standards that everyone conforms to.
These processes and standards could be anything that as a team you feel should be decided upon to prevent confusion; for example, the casing of file names, how CSS should be done, how variables should be named, the length of functions, and when comments should be used, etc.
Then finally, the most important part is holding everyone accountable for the decisions made. Once agreed upon these standards and processes should be checked and maintained in PRs; if the code doesn’t follow them then it shouldn’t get merged. I know it seems like a lot of pain and not a lot of gain in the short term but in the long run, having a uniform codebase and working environment is liberating for everyone on the project. People can focus more on building creative solutions to problems and less on making decisions about how files should be named.
Do your best, gradually
In this post, we’ve covered eight best practices for writing high-quality code that is scalable and promotes healthy collaboration in a team project, while not hindering your resources or coding abilities. It’s important to note that you don’t need to jump in the deep end and instantly apply every coding and project best practice out there overnight. I’m sure there would be a lot of pushback if that was attempted. Instead, try to incrementally adopt the best practices that suit your project and benefit you and your team the most. Then, over time adopt more best practices as they make sense.