Over my 26 years of programming, developing applications, snippets, servers, APIs etc, I have been through a lot of different coding setups, editors, environments and so on. To sum up parts of my experience, I am attempting to disclose to you how you can have the best frontend developer experience.
However, I am not going to tell you how to set up your editor directly. Every developer has a different opinion on which extension, theme, setting etc that is best, my self included.
- Standardize
- Automate
- Extrapolate
- Minimize
- Conclusion
Standardize
I love standards, especially standards that promotes effecient workflows, clean code and helps both the individual programmers and cooperating teams when developing applications. These are the standards I found to give me the best frontend developer experience:
Semantic versioning
As described on the Semantic Versioning website, semantic versioning (semver) consists of three numbers: MAJOR, MINOR, and PATCH. Each number is incremented in different circumstances:
- the MAJOR version when we make incompatible API changes (read: breaking changes),
- the MINOR version when we add functionality in a backward-compatible manner, and
- the PATCH version when we make backward-compatible bug fixes.
Using semver helps a great deal for developers, and non-developers to understand what has changed, since the changed number in the version reflects what the actual change consists of.
- Is it something that I need to deal with? Do I need to make changes in my own code for the change to work on my end?
- Is it a new feature that I could check out?
- Is it a bug fix, documentation update, or any non-important change?
Commits
The perfect commit message should have certain qualities:
- It should be understandable even by seeing only the header of the message
- It should be to the point, and not too detailed.
- It should be unambiguous.
Use the imperative, present tense. It is easier to read and scan quickly:
The reason behind using present tense is that the commit message is answering the question "What will happen after the commit is applied?". If we think of a commit as an independent patch, it does not matter if it applied in the past. What matters is that this patch is always supposed to make that particular change when it is applied.
Commitizen
Commitizen helps you write better commit messages!
Install it:
Update package.json
:
After you have staged your files for commit, run npm run commit
or yarn commit
in your project root folder. You will get an interactive guide that helps you with your commits:
When you have chosen the type of commit you are commiting, press enter, and you can now enter a commit header:
After you have pressed enter, you can now enter an optional commit body, that has more space to descrive the change the commit adds:
After that, you will get an option to indicate if the change is a breaking change or not. If the change is NOT a breaking change, leave this field empty:
You now have an option to add an issue ID for tracking the commit to a relevant task. This works for Jira (if you have the service connection) and github issues.
When pressing enter, the interactive tool will stop, and output the commit output:
When you now do a git log
, you'll see the change:
If you entered a description and/or a issue ID, it could look like this:
Linting
I think that linting, when the concept arrived, was for me one of the biggest features for me as a developer. It helped a great deal for project code that was worked on in a team, conforming to one set of format. It also made the code look and feel cleaner.
My favourite setup for now, is to use eslint
in combination with prettier
prettier
+ eslint
= prettier-eslint
β₯
Preferred plugins:
For TypeScript projects, add these as well:
My standard .eslintrc.json
:
My standard .prettierrc
:
Why 80
as printWidth
when most of the devs use widescreens?
The reason why prettier uses 80 columns to format code is because this is the best heuristic we know. It's not perfect as you have seen but one interesting property is that it almost never looks terrible and most of the time it look reasonable.
And
The longer the line, the bigger the diff and the harder it is to track what has been chanced
My standard .stylintrc
Functions
We all have our preferences when it comes to functions, these are some of mine.
Naming
I prefer readability, so I would like to read the function name, and know exactly what it does. When it comes to boolean functions or methods, I like to read them as the state they want to represent.
Return early
If you have conditional returns in a function or method, I love to pull out early, and it makes it, for me, easier to follow the flow. Especially if you calculations that is not required to do before escaping.
Nested ternary operators
Ternary operators are great, but use it with caution, because it can lead to horrible readability.
JSX
Please, do not use any logix in jsx! It promotes poor readability, and increases the number of lines in the file.
Imports
Aliasing
If you have a large frontend applications, imports could be a huge mess, with long imports like:
If you use TypeScript, you can add aliases in the tsconfig.json
:
And for webpack:
So that the import would look like this instead:
Sorting and grouping of imports
To be able to group imports, and to make the hierarchy clearer, you can use the import/order
rule with ESLint:
Which would make this:
turn into this:
Nomenclature
Having a good naming convention is key in making logical sense of the code, and makes it easier to grasp the concept of the code and to get a better overview over what the code does.
Please, for the love of Cthulhu, do not have your naming convention like this:
The import would then look like:
Using an unecessary amount of bytes. It is better to do this:
The import would then look like:
It's cleaner, shorter and saves bytes!
Folder structure
Establishing a well-organized and sensible folder structure is one of the most crucial and demanding aspects of managing a large-scale application. Before contemplating the division of the codebase into multiple applications using micro frontends, it is advisable to take certain steps to enhance the project-level architecture. This approach can facilitate a smoother transition if such a path is ever considered.
The objective is to implement a form of modularization that enhances the comprehensibility of the codebase by defining clear boundaries between features. This strategy aims to minimize code coupling and reduce side effects for improved maintainability.
This is how I recommend to set it up, based on what I feel works best for me. YMMV:
Applications
JavaScript Web Application
TypeScript Web Application
React Web Application
NodeJS Application
public
All public files, but most likely, only index.html
or any public static file.
scripts
Put helper scripts for inhouse usage/auxilliary tools here.
src/assets
All your static assets goes here. This includes:
- prebuilt libs in
src/assets/js
- images in
src/assets/img
- icons in
src/assets/icons
(orsrc/assets/img/icons
) - prebuilt stylesheets (
*.css
) insrc/assets/css
- fonts in
src/assets/fonts
- meta stuff like
browserconfig.xml
/site.webmanifest
insrc/assets/meta
src/components
Shareable components goes here.
Grouping
These components should be grouped by the type of component, for example:
- link, menus etc goes into
src/components/navigation
- header, footer, main, intro goes into
src/components/page-section
- input fields, form stuff goes into
src/components/form
orsrc/components/input-elements
- tables, data-tables, comparison tables goes into
src/components/tables
- errors, loading, notification, toasts goes into
src/components/feedback
Hierarchy
If a component requires other components only specific to that component, you should have a child folder named components
. For example, if you have a component named Table
in src/components/tables/Table
, and you want TableHeader
, TableFooter
, TableSort
components, put them in src/components/tables/Table/components/TableHeader/
etc.
However, if you have only one extra component, put it alongside the main component
If a *.tsx/*.jsx
context, if a component require a set of code that returns different components based on input, it is a helper, and should be in src/components/tables/Table/helpers/
:
Each component can have a set of folders, similar to the src
setup:
src/config
Any configuration for your application goes here (not test configs or configs that should be in the project root).
src/contexts
If you use a framework that uses contexts, put them here.
src/features
The majority of the code should be placed in this location. To facilitate smoother maintenance and scalability, our objective is to house the bulk of the application code within the features folder. Each feature folder is intended to encompass domain-specific code related to a particular feature, ensuring organizational clarity.
src/services
Shared application services and providers
src/lib
Config for 3rd party libs goes here, or if you require to have your own version of a 3rd party lib, and also have to compile it, put it here. If the 3rd party lib is not to be compiled by you, put it in src/assets/js/
.
src/pages
Put pages here. Having all the pages in one place is very helpful but the logic inside them should be kept to the minimum.
src/styles
Put global styles here.
src/stores
If you use stores, put them here.
src/test
Test related stuff, mocks, helpers, utilities etc.
src/types
If you use TypeScript, puth types, interfaces and enums here.
src/utils
If the code is some standalone, drop in function or class, put it here.
Automate
I love to get rid of secondary activites, L O V E it. So if I can automate it, I automate it!
Changelog
Changelog generation goes hand in hand with releases, or changes merged into the master. And since I use commitizen
, creating changelogs is a walk in the park. I use Conventional Changelog with conventional-changelog-conventionalcommits
.
Releases
To orchestrate my releases, i use release-it
, and it's basically fire and forget. You can do it with an interactive CLI, or like me, in an action.
Update your scripts in package.json
:
And you can do npm run release
!
This is the .release-it.json
I use:
CI/CD
Use a decent CI/CD setup, I love GitHub Actions.
I always use these actions, regardless of the type of project I am working on:
Extrapolate
Documentation generation
If you use JSDoc, you can generate documentation based on that. If you use TypeScript, you can have automagic documentation generated with TypeDoc.
My preferred plugins:
Update your package.json
scripts:
This will generate documentation based of your code and JSdoc annotation in the /api
folder.
Minimize
Why advocate for code minimization? Well, a primary objective in my approach to development, both professionally and as a human being, is to contribute to a better world. Streamlining and reducing code directly contribute to a lower carbon footprint by using fewer bytes and transmitting less data over the internet.
While many developers already focus on optimizing applications for speed, the emphasis on sustainability is gaining prominence. The equation is straight forward: fewer bytes sent result in a faster application, leading to a reduced carbon footprint. It's a win-win scenario.
To enhance your carbon footprint, you can apply the same principles used for optimizing and minimizing code. Additionally, there are other strategies to lessen the environmental impact in development, including:
- General reduction of code
- Minimizing the code stored in your repository
- Decreasing the number of dependencies in your project
- Reducing the frequency of builds or the shipment of artifacts to other vendors
Reduce code
Repeat after me: Refactor, refactor, refactor. Continusly refactoring and pruning the application is to me kinda like the process of caring for a bonsai tree. Create reusable components, split out code, carefully select dependencies, IF you require them.
Smaller repositories
In the mantra of reducing code. Do not curate a large repository, consider splitting up the repository in smaller repositories. If some parts of the code is rarely updated, split it out. If parts of your code could be reusable in other projects, split it out. Consider to open source it!
You could also test out monorepos, for example with Lerna.
Reduce dependencies
Do not use dependencies for code you can create yourself (leftpad anyone?). Many dependencies are created as polyfills for features a native API did not have in the beginning, but they have support for it now. Like jQuery, moment, date-fns and lodash/underscore.
Usefull links:
- https://youmightnotneedjquery.com/
- https://github.com/you-dont-need/You-Dont-Need-Momentjs
- https://youmightnotneed.com/lodash
- https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore
- https://youmightnotneed.com/date-fns
Reduce builds
Ask yourself these questions:
- Do you really need to install every dependency for every build? Can you reuse some artifacts instead?
- Do you really need to update snapshots?
- Do you have to build for every release stage?
One example is, when you create a PR to main
, you have a merge check, and most likely in that merge check, you have a build step and a test step. If the check is passing, do you really need to to a new check on main
?
Another example, if the PR only have changes in files that in no way effect the frontend application features/look/appearance, do you really need to have a merge-check?
Conclusion
In conclusion, achieving the best frontend developer experience involves a combination of standardization, automation, and code minimization. Standardizing practices, such as adopting semantic versioning for clear communication about changes, committing to readable and concise commit messages, and following consistent coding patterns, contributes to an efficient workflow and collaborative development.
Linting, particularly using tools like ESLint in conjunction with Prettier, enhances code quality and maintains a clean and uniform codebase. Establishing meaningful naming conventions, returning early in functions, and avoiding nested ternary operators improve code readability and maintainability.
Organizing code into a well-structured folder hierarchy, utilizing aliases for imports, and employing sorting and grouping for imports contribute to a more maintainable codebase. Additionally, having a clear folder structure, including features, services, pages, and utilities, is essential for managing large-scale applications.
Automation plays a crucial role in streamlining development processes. Leveraging tools like Commitizen for standardized commit messages, Release-it for automated releases, and GitHub Actions for continuous integration and deployment enhances efficiency and reduces manual intervention.
Code minimization, both in terms of reducing code size and optimizing development processes, is highlighted as a key principle. Refactoring code, adopting smaller repositories, minimizing dependencies, and optimizing build processes contribute to a smaller carbon footprint and a more sustainable development approach.
In essence, the journey towards a better frontend developer experience involves a holistic approach that encompasses standardized practices, efficient automation, and a commitment to code minimization for improved sustainability and maintainability.