A set of best practices for JavaScript projects
Maintaining someone else’s code is not a smooth process. It takes time to observe the project (folder structure, naming, dependencies, scripts etc.), find the pattern and develop the new feature in harmony and consistency with the existing code. Different developers use different styles which are derived from their different tastes. They may work on a project together or pick up someone else’s work. Which in both cases, having a common ground is essential.
That’s why at Elsewhen we decided to come up with a set of common practices for everyone to follow. Maintenance is easier as it requires less time analysing the existing codebase to realise what’s going on. The result is a list of guidelines which looks random and is not perfect. But we try to stick to it and improve it by making it public. It covers various aspects of a project. A more detailed version is hosted on github in two languages, and constantly gets updated.
Project Guidelines:
Because while developing a new project is like rolling on a green field for you, maintaining it is a potential dark twisted nightmare for someone else. Here’s a list of guidelines we’ve found, written and gathered that (we think) works really well with most JavaScript projects.
1. Git
1.1 Some Git rules
There are a set of rules to keep in mind:
- Perform work in a feature branch.
Because this way all work is done in isolation on a dedicated branch rather than the main branch. It allows you to submit multiple pull requests without confusion. You can iterate without polluting the master branch with potentially unstable, unfinished code. read more…
- Branch out from
develop
Because you can make sure that code in master will almost always build without problems, and can be mostly used directly for releases (this might be overkill for some projects).
- Never push into
develop
ormaster
branch. Make a Pull Request.
Because it notifies team members that they have completed a feature. It also enables easy peer-review of the code and dedicates forum for discussing the proposed feature
- Update your local
develop
branch and do an interactive rebase before pushing your feature and making a Pull Request
Because rebasing will merge in the requested branch (
master
ordevelop
) and apply the commits that you have made locally to the top of the history without creating a merge commit (assuming there were no conflicts). Resulting in a nice and clean history. read more ...
- Resolve potential conflicts while rebasing and before making a Pull Request
Just because
- Delete local and remote feature branches after merging.
Because it will clutter up your list of branches with dead branches.It insures you only ever merge the branch back into (
master
ordevelop
) once. Feature branches should only exist while the work is still in progress.
- Before making a Pull Request, make sure your feature branch builds successfully and passes all tests (including code style checks).
Because you are about to add your code to a stable branch. If your feature-branch tests fail, there is a high chance that your destination branch build will fail too. Additionally you need to apply code style check before making a Pull Request. It aids readability and reduces the chance of formatting fixes being mingled in with actual changes.
- Use this
.gitignore
file.
Because it already has a list of system files that should not be sent with your code into a remote repository. In addition, it excludes setting folders and files for most used editors, as well as most common dependency folders.
- Protect your
develop
andmaster
branch.
Because it protects your production-ready branches from receiving unexpected and irreversible changes. read more… Github and Bitbucket
1.2 Git workflow
Because of most of the reasons above, we use Feature-branch-workflow with Interactive Rebasing and some elements of Gitflow (naming and having a develop branch). You can read more about the steps we follow here.
2. Documentation
- Use this template for
README.md
, Feel free to add uncovered sections. - For projects with more than one repository, provide links to them in their respective
README.md
files. - Keep
README.md
updated as a project evolves. - Comment your code. Try to make it as clear as possible what you are intending with each major section.
- If there is an open discussion on github or stackoverflow about the code or approach you’re using, include the link in your comment.
- Don’t use comments as an excuse for a bad code. Keep your code clean.
- Don’t use clean code as an excuse to not comment at all.
- Keep comments relevant as your code evolves.
3. Environments
- Define separate
development
,test
andproduction
environments if needed.
Because different data, tokens, APIs, ports etc… might be needed on different environments. You may want an isolated
development
mode that calls fake API which returns predictable data, making both automated and manually testing much easier. Or you may want to enable Google Analytics only onproduction
and so on. read more...
- Load your deployment specific configurations from environment variables and never add them to the codebase as constants, look at this sample.
Because you have tokens, passwords and other valuable information in there. Your config should be correctly separated from the app internals as if the codebase could be made public at any moment.
How? use
.env
files to store your variables and add them to.gitignore
to be excluded. Instead, commit a.env.example
which serves as a guide for developers. For production, you should still set your environment variables in the standard way. read more here by Rafael Vidaurre.
- It’s recommended to validate environment variables before your app starts. Look at this sample using
joi
to validate provided values.
3.1 Consistent dev environments:
- Set your node version in
engines
inpackage.json
Because it lets others know the version of node the project works on. read more…
- Additionally, use
nvm
and create a.nvmrc
in your project root. Don't forget to mention it in the documentation
Because any one who uses
nvm
can simply usenvm use
to switch to the suitable node version. read more...
- It’s a good idea to setup a
preinstall
script that checks node and npm versions
Because some dependencies may fail when installed by newer versions of npm.
- Use Docker image if you can.
Because it can give you a consistent environment across the entire workflow. Without much need to fiddle with dependencies or configs. read more…
- Use local modules instead of using globally installed modules
Because it lets you share your tooling with your colleague instead of expecting them to have it globally on their systems.
3.2 Consistent dependencies:
- Make sure your team members get the exact same dependencies as you
Because you want the code to behave as expected and identical in any development machine read more here by Kent C. Dodds.
Usepackage-lock.json
onnpm@5
or higher. Alternatively you can useYarn
. For older versions ofnpm
, use—save --save-exact
when installing a new dependency and createnpm-shrinkwrap.json
before publishing. read more...
4. Dependencies
- Keep track of your currently available packages: e.g.,
npm ls --depth=0
. read more... - See if any of your packages have become unused or irrelevant:
depcheck
. read more...
Because you may include an unused library in your code and increase the production bundle size. Find unused dependencies and get rid of them.
- Before using a dependency, check its download statistics to see if it is heavily used by the community:
npm-stat
. read more...
Because more usage mostly means more contributors, which usually means better maintenance, and all of these result in quickly discovered bugs and quickly developed fixes.
- Before using a dependency, check to see if it has a good, mature version release frequency with a large number of maintainers: e.g.,
npm view async
. read more...
Because having loads of contributors won’t be as effective if maintainers don’t merge fixes and patches quickly enough.
- If a less known dependency is needed, discuss it with the team before using it.
- Always make sure your app works with the latest version of its dependencies without breaking:
npm outdated
. read more...
Because dependency updates sometimes contain breaking changes. Always check their release notes when updates show up. Update your dependencies one by one, that makes troubleshooting easier if anything goes wrong. Also, you can use a cool tool such as npm-check-updates to keep track of dependency updates (Thanks to Raine Rupert Revere).
- Check to see if the package has known security vulnerabilities with, e.g., Snyk.
5. Testing
- Have a
test
mode environment if needed.
Because while sometimes end to end testing in
production
mode might seem enough, there are some exceptions: One example is you may not want to enable analytical information on a 'production' mode and pollute someone's dashboard with test data. The other example is that your API may have rate limits inproduction
blocks your test calls after a certain amount of requests.
- Place your test files next to the tested modules using
*.test.js
or*.spec.js
naming convention, likemoduleName.spec.js
Because you don’t want to dig through a folder structure to find a unit test. read more…
- Put your additional test files into a separate test folder to avoid confusion.
Because some test files don’t particularly relate to any specific implementation file. You have to put it in a folder that is most likely to be found by other developers:
__test__
folder. This name:__test__
is also standard now and gets picked up by most JavaScript testing frameworks.
- Write testable code, avoid side effects, extract side effects, write pure functions
because you want to test a business logic as separate units. You have to “minimize the impact of randomness and nondeterministic processes on the reliability of your code”. read more…
A pure function is a function that always returns the same output for the same input. Conversely, an impure function is one that may have side effects or depends on conditions from the outside to produce a value. That makes it less predictable read more…
- Use a static type checker
Because it brings a certain level of reliability to your code. read more here by Preethi Kasireddy.
- Run tests locally before making any pull requests to
develop
.
Because you don’t want to be the one who caused production-ready branch build to fail. Run your tests after your
rebase
and before pushing your feature-branch to a remote repository.
- Document your tests including instructions in the relevant section of your
README.md
file.
Because it’s a handy note you leave behind for other developers or DevOps experts or QA or anyone who gets lucky enough to work on your code.
6. Structure and Naming
- Organize your files around product features / pages / components, not roles. Also, place your test files next to their implementation.
// Bad
.
├── controllers
| ├── product.js
| └── user.js
├── models
| ├── product.js
| └── user.js
// Good
.
├── product
| ├── index.js
| ├── product.js
| └── product.test.js
├── user
| ├── index.js
| ├── user.js
| └── user.test.js
Because instead of a long list of files, you will create small modules that encapsulate one responsibility including its test and so on. It gets much easier to navigate through and things can be found at a glance.
- Use a
./config
folder and don't make different config files for different environments.
Because when you break down a config file for different purposes (database, API and so on); putting them in a folder with a very recognizable name such as
config
makes sense. Just remember not to make different config files for different environments. It doesn't scale cleanly, as more deploys of the app are created, new environment names are necessary. Values to be used in config files should be provided by environment variables. read more here by Fedor Korshunov.
- Put your scripts in a
./scripts
folder. This includesbash
andnode
scripts.
Because it’s very likely that you end up with more than one script, production build, development build, database feeders, database synchronization and so on.
- Place your build output in a
./build
folder. Addbuild/
to.gitignore
.
Name it what you like,
dist
is also cool. But make sure that keep it consistent with your team. What gets in there is most likely generated (bundled, compiled, transpiled) or moved there. What you can generate, your teammates should be able to generate too, so there is no point committing them into your remote repository. Unless you specifically want to.
- Use
PascalCase' 'camelCase
for filenames and directory names. UsePascalCase
only for Components. CheckBox/index.js
should have theCheckBox
component, as couldCheckBox.js
, but notCheckBox/CheckBox.js
orcheckbox/CheckBox.js
which are redundant.- Ideally the directory name should match the name of the default export of
index.js
.
Then you can expect what component or module you will receive by simply just importing its parent folder.
7. Code style
- Use stage-2 and higher JavaScript (modern) syntax for new projects. For old project stay consistent with existing syntax unless you intend to modernise the project.
Because this is all up to you. We use transpilers to use advantages of new syntax. stage-2 is more likely to eventually become part of the spec with only minor revisions.
- Include code style check in your build process.
Because breaking your build is one way of enforcing code style to your code. It prevents you from taking it less seriously. Do it for both client and server-side code. read more…
- Use ESLint — Pluggable JavaScript linter to enforce code style.
because we simply prefer
eslint
, you don't have to. It has more rules supported, the ability to configure the rules, and ability to add custom rules.
- We use Airbnb JavaScript Style Guide for JavaScript, Read more. Use the javascript style guide required by the project or your team.
- We use Flow type style check rules for ESLint. when using FlowType.
Because Flow introduces few syntaxes that also need to follow certain code style and be checked.
- Use
.eslintignore
to exclude file or folders from code style check.
Because you don’t have to pollute your code with
eslint-disable
comments whenever you need to exclude a couple of files from style checking.
- Remove any of your
eslint
disable comments before making a Pull Request.
Because while it’s normal to disable style check while working on a code block to focus more on the logic you should remember to remove those
eslint-disable
comments and follow the rules.
- Depending on the size of the task use
//TODO:
comments or open a ticket.
Because then you can remind yourself and others about a small task (like refactoring a function, or updating a comment). For larger tasks use
//TODO(#3456)
which is enforced by a lint rule and the number is an open ticket.
- Always comment and keep them relevant as code changes. Remove commented blocks of code.
Because your code should be as readable as possible, you should get rid of anything distracting. If you refactored a function, don’t just comment out the old one, remove it.
- Avoid irrelevant or funny comments, logs or naming.
Because while your build process may(should) get rid of them, your source code may get handed over to another company/client and they may not share the same banter.
- Make your names search-able with meaningful distinctions avoid shortened names. For functions Use long, descriptive names. A function name should be a verb or a verb phrase, and it needs to communicate its intention.
Because it makes it more natural to read the source code.
- Organize your functions in a file according to the step-down rule. Higher level functions should be on top and lower levels below.
Because it makes it more natural to read the source code.
8. Logging
- Avoid client-side console logs in production
Because while your build process can(should) get rid of them, you should make sure your code style check gives you warning about console logs.
- Produce readable production logging. Ideally use logging libraries to be used in production mode (such as winston or node-bunyan).
because it makes your troubleshooting less unpleasant with colorization, timestamps, log to a file in addition to the console or even logging to a file that rotates daily. read more…
9. API
Because we try to enforce development of sanely constructed RESTful interfaces, which team members and clients can consume simply and consistently. Lack of consistency and simplicity can massively increase integration and maintenance costs. Which is why
API design
is included in this article.
- We mostly follow resource-oriented design. It has three main factors: resources, collection, and URLs. A resource has data, gets nested, and there are methods that operate against it. A group of resources is called a collection. URL identifies the online location of resource or collection.
Because this is a very well-known design to developers (your main API consumers). Apart from readability and ease of use, it allows us to write generic libraries and connectors without even knowing what the API is about.
- Use kebab-case for URLs.
- Use camelCase for parameters in the query string or resource fields.
- Use plural kebab-case for resource names in URLs.
- Always use a plural nouns for naming a url pointing to a collection:
/users
.
Because it reads better and keeps URLs consistent. read more…
- In the source code convert plurals to variables and properties with a List suffix.
Because plural may look nice in the URL but in the source code, it’s just too subtle and error-prone.
- Always use a singular concept that starts with a collection and ends to an identifier:
/students/245743
/airports/kjfk
- Avoid URLs like this:
GET /blogs/:blogId/posts/:postId/summary
Because this is not pointing to a resource but to a property instead. You can pass the property as a parameter to trim your response.
- Keep verbs out of your resource URLs.
Because if you use a verb for each resource operation you soon will have a huge list of URLs and no consistent pattern which makes it difficult for developers to learn. Plus we use verbs for something else
- Use verbs for non-resources. In this case, your API doesn’t return any resources. Instead, you execute an operation and return the result. These are not CRUD (create, retrieve, update, and delete) operations:
/translate?text=Hallo
Because for CRUD we use HTTP methods on
resource
orcollection
URLs. The verbs we were talking about are actuallyControllers
. You usually don't develop many of these. read more...
- The request body or response type is JSON then please follow
camelCase
forJSON
property names to maintain the consistency.
Because if this is a JavaScript project guideline, Programming language for generating JSON as well as Programming language for parsing JSON are assumed to be JavaScript.
- Even though a resource is a singular concept that is similar to an object instance or database record, you should not use your
table_name
for a resource name andcolumn_name
resource property.
Because your intention is to expose Resources, not your database schema details
- Again, only use nouns in your URL when naming your resources and don’t try to explain their functionality. Avoid endpoints like
/addNewUser
or/updateUser
. Also avoid sending resource operations as a parameter. - Explain the CRUD functionalities using HTTP methods:
GET
: To retrieve a representation of a resource.POST
: To create new resources and sub-resourcesPUT
: To update existing resourcesPATCH
: To update fields of existing resources.DELETE
: To delete existing resources
- For nested resources, use the relation between them in the URL. For instance, using
id
to relate an employee to a company.
Because this is a natural way to make resources explorable.
- Use a simple ordinal number for a version with a
v
prefix (v1, v2). Move it all the way to the left in the URL so that it has the highest scope:
http://api.domain.com/v1/schools/3/students
When your APIs are public for other third parties, upgrading the APIs with some breaking change would also lead to breaking the existing products or services using your APIs. Using versions in your URL can prevent that from happening. read more…
- Response messages must be self-descriptive. A good error message response might look something like this:
{
"code": 1234,
"message" : "Something bad happened",
"description" : "More details"
}
Because developers depend on well-designed errors at the critical times when they are troubleshooting and resolving issues after the applications they’ve built using your APIs are in the hands of their users.
- Use only these 8 status codes to send with you response to describe whether everything worked, The client app did something wrong or The API did something wrong:
200 OK
response represents success forGET
,PUT
orPOST
requests.201 Created
for when new instance is created.304 Not Modified
recipient already has cached representations.400 Bad Request
for when the request was not processed.401 Unauthorized
403 Forbidden
404 Not Found
500 Internal Server Error
Because most API providers use a small subset HTTP status codes. For example, the Google GData API uses only 10 status codes, Netflix uses 9, and Digg, only 8. Of course, these responses contain a body with additional information.There are over 70 HTTP status codes. However, most developers don’t have all 70 memorized. So if you choose status codes that are not very common you will force application developers away from building their apps and over to wikipedia to figure out what you’re trying to tell them. read more…
- Provide total numbers of resources in your response
- Accept
limit
andoffset
parameters - The amount of data the resource exposes should also be taken into account. The API consumer doesn’t always need the full representation of a resource.Use a fields query parameter that takes a comma separated list of fields to include:
GET /student?fields=id,name,age,class
Sources: RisingStack Engineering, Mozilla Developer Network, Heroku Dev Center, Airbnb/javascript, Atlassian Git tutorials, Apigee, Wishtack and Medium.
Awesome Icons by icons8
Well, that’s it folks :). I know it is very opinionated, please feel free to challenge it in the comments. Checkout the repo if you have time, open an issue, make a pull requests.
You can follow me, here. You can also follow @elsewhen where we tweet about tech, product and design related stuff.