Imagine the workshop of a craftsman. Every tool, every material, and every detail is precisely where it belongs — carefully arranged and meticulously maintained. There is no chaos, no disorder — only focused, clear, and efficient work. I’ve always aspired for my projects to embody that same quality.
In the realm of software development, where complex dependency management systems, configuration files, and automated build processes dominate, the simple act of copying library files into a project might seem antiquated. Yet, I believe that the simplest approaches are often the most elegant and effective. What could be simpler than copying files? This process embodies a sense of elegance and control — qualities that are frequently overshadowed by the complexities of modern development workflows.
When the documentation for a library starts with "use a dependency manager," it immediately raises a few concerns. First, such an instruction creates the impression that the library is complex and difficult to navigate. The need for a dependency manager suggests that the library has numerous dependencies, which can complicate its integration and usage.
Second, it makes me think that the files I’ll encounter after installing the library will contain only part of the code needed for it to function. A significant portion of the library’s functionality may be hidden inside packages managed by the dependency manager. As a result, understanding how the library is structured and how it works under the hood will require studying all the packages, which takes time.
I prefer simple, transparent access to the code. The only reasonable way to add a library to a project, in my view, is to copy the library into a directory of my choosing and link its header file where necessary in the project.
Moreover, if a dependency manager is eventually required for a specific project, it’s much easier to switch from static inclusion to dynamic inclusion than to do the reverse.
How to Organize a Project
When I created the KoiCom library, I wanted a simple and clear library with no external dependencies. You can copy the library files into any directory you prefer and include only the specific component you need — nothing more. There’s no need to integrate the entire functionality of the library into your project. You don’t need a package manager, nor do you need to follow any special file structure in your project.
However, for my own convenience, I use the following structure, to which I’m accustomed. I’m not imposing it on you, but I’m presenting it here as an example to illustrate the principles outlined further.
- css
- controls
- tables
- sample_table.css
- ...
- ...
- tables
- template
- layout.css
- ...
- controls
- js
- project_name
- controls
- tables
- sample_table.js
- ...
- ...
- tables
- providers
- csv
- sample_csv_connector.js
- ...
- ...
- csv
- controls
- project_name
- libs
- web-components-lib
- controls
- providers
- web-components-lib
Components are not their appearance
You may immediately notice that the appearance (CSS) is strictly separated from the directory containing the components (JS) and from the library itself (libs). This is primarily because, in my paradigm, what behaves like a duck is a duck, regardless of its appearance. A button can look any way — it can be rectangular, round, or resemble a link — but it’s still a button because it has a visual representation, it can be clicked, and clicking it triggers an event. It is this behavior, and the lack of any other behavior, that defines it as a button.
I want to be able to apply any design to the button at any time, altering its typography, size, or position on the page, and this change in appearance should not affect the button’s behavior in any way.
Following this principle, the CSS files that define the appearance are strictly separated from the files that implement the button’s behavior.
It might seem more convenient to keep the CSS file next to the component’s code to avoid having to go to a separate directory. However, in my experience, it is quite rare to work simultaneously with both JS code and CSS. It happens much less often than working with the CSS of one component alongside the CSS of another. For this reason, it’s better to keep the CSS files together. Moreover, this approach makes it easy to swap one theme for another by simply copying the new theme’s directory into the project.
The abundance of files is not a problem
You may also notice that each button has its own CSS file, named the same as the control instance. Using consistent naming with appropriate prefixes and suffixes helps avoid confusion and makes it easy to locate the files.
The abundance of CSS files should not be a concern either. When preparing the project for production, it's not difficult to write a script that consolidates and compresses them. However, it is convenient to work with the CSS of a specific component, without being distracted by the rest of the project's CSS.
The same logic applies to JS code files. As development progresses, hypotheses need to be tested, new ideas experimented with, and progress needs to be documented, all while separating classes and extracting behaviors. The number of concepts and relationships grows, and the simplest way to catalog this growing volume is through the file system. That’s why any advanced project consists of a large number of files. Fortunately, isolating potentially stable code and encapsulating it enables the creation of a library directory structure.
Therefore, the abundance of files should not be viewed as a problem, but attention should be given to proper cataloging.
The project inherits from the library
In the structure outlined above, a project directory is designated, which contains files that implement the behavior of specific components used in the project. These files are separate from the library files.
The code that implements a specific project is almost always experimental. Some functionality is created hastily with the goal of finishing the project quickly, while other parts are developed to test new approaches. This code, until properly cataloged, will often be a collection of various and sometimes incomplete files.
To separate this collection from the cataloged and library-structured files, I decided to store such code in the js directory, taking care to organize it into subdirectories named according to the purpose of the code.
In my view, this resulting structure ensures that the project-specific code, which implements particular behaviors, is, on the one hand, kept separate from the library files, and on the other hand, can be customized to meet the specific requirements of the project without being constrained by the library’s capabilities.
How the MVC Approach is Used
If you look closely at the structure, you’ll notice the complete absence of any reference to MVC. Honestly, I like MVC. I’m fully in favor of MVC. But the specific implementation of this principle in the frameworks I’m familiar with is discouraging.
For some reason I can’t understand, every new framework starts with MVC, and right from the start, I’m forced to store templates in one place, model code in another, and routes in yet another. As a result, when a method name in the model changes, I have to go to a different directory, open the route, and change the method call. This is tedious. It becomes even more frustrating when a change in the controller logic requires modifying the model logic, which is located in a different directory.
And don’t tell me that IDEs solve this problem. On remote servers, I often find myself working with VIM or NANO. Please write code so that errors can be fixed without the use of an IDE.
So, how do I suggest we proceed? I start from the idea that a component is created specifically to encapsulate certain behaviors within a class and to hide the implementation details. Public methods are typically controller methods, while the model and view are encapsulated. Therefore, I recommend applying the MVC principle within the component itself, in its own directory.
In other words, each component should have its template, controller, and model separated but not scattered across the project.
For this reason, I wasn’t too upset when I found out that templating in web components isn’t as good as it could be. The authors of books on web components tend to focus too much on the idea of templating. I recommend ignoring the concepts of templates and slots for web components. And you certainly won’t need Shadow DOM, but we’ll discuss that later. The real strength of web components lies elsewhere.
Structure Features
The last point I want to draw your attention to is the levels of nesting.
When using import, you need to specify the path to the file from which you are importing classes. This can be either an absolute or a relative path. In either case, the file from which you are importing modules must be located at a certain level of nesting, either relative to the root of the project or relative to the file containing the import statement. In other words, the code performing the import must know where the class it is importing is located.
This seems logical, but if you’re working on multiple projects, or if your project has a custom build for one or more clients, your main code transforms from project-specific code to a library that you include in your project.
At some point, you’ll want to move the library from the js directory to the libs directory because you’ve realized that your library is becoming more universal. To avoid rewriting paths in all library files and simply copy the directory from js to libs, the controls and providers directories of your library should initially be at the same nesting level as the controls and providers directories in the KoiCom library. For this reason, the structure of my projects initially has a js/project_name nesting level.
In other words, when starting work on each new project, I plan from the outset to write not just a specific project, but a library that I can use in the future.
Conclusion
To summarize, I’ll highlight my most important point: I wrote the library code with the goal that it could be interacted with using the most basic tools, such as system utilities and simple editors. The second key point: how a component behaves and how it looks are two completely separate abstractions, and each should be dealt with separately. The third key point: when starting a project, create not just a project, but a library.