Greetings, I'm Suraj, and I became part of Fyle in May 2023, serving as a Senior Member of the Technical Staff within the Frontend Engineering Team.
Upon my arrival, we were transitioning from the legacy AngularJS stack to the more modern Angular versions.
If you're curious about our migration journey, I recommend checking out an excellent blog on the topic by my friend Aditya Baddur aka AB
Overview of Fyle's Frontend Applications :
The Fyle suite comprises diverse applications tailored to different user personas and needs, including accounts, admin, spender, settings, upsert expenses, and developers.
We decided to opt for NX as a monorepo tool to consolidate all these distinct apps and we chose Tailwind CSS for styling.
How our folder structure looked earlier, when we just integrated Nx :
.
└── app-v2/
└── apps/
├── accounts/
│ ├── src/
│ │ └── app/
│ │ ├── ...
│ │ └── index.html
│ └── tailwind.config.js
├── admin/
│ ├── src/
│ │ └── app/
│ │ ├── ...
│ │ └── index.html
│ └── tailwind.config.js
├── spender/
│ ├── src/
│ │ └── app/
│ │ ├── ...
│ │ └── index.html
│ └── tailwind.config.js
├── upsert-expenses/
│ ├── src/
│ │ └── app/
│ │ ├── ...
│ │ └── index.html
│ └── tailwind.config.js
├── settings/
│ ├── src/
│ │ └── app/
│ │ ├── ...
│ │ └── index.html
│ └── tailwind.config.js
└── developers/
├── src/
│ └── app/
│ ├── ...
│ └── index.html
└── tailwind.config.js
Initially, this structure served well as the starting point for our migration process.
However, we soon recognized a few challenges that we were about to encounter as the code base expanded.
Here are some of the challenges we encountered:
Each app operates in isolation, lacking a mechanism to share common code across different projects.
We employ our proprietary design language called Fyle Design Language (FDL), which has specific design constraints. These constraints are outlined in the tailwind.config.js file of each app to generate Tailwind CSS classes. Notably, the Tailwind configurations vary among apps since each one is maintained by a different team.
There was a considerable amount of code repetition across various apps for services, components, and other elements.
There is no rigid structure in place for naming conventions regarding directories and files and other stuff like component selectors.
The above-mentioned challenges lead to a less-than-optimal developer experience.
Up to this point, I trust I've provided you with a basic walkthrough of the current state of the application and the challenges we are encountering.
Discovering the Solution:
We opted to address the challenges right from the foundational stages of the design process. We aimed to embrace the philosophy of "What you see in Figma is what you get in code.”
Introducing Design Tokens for crafting the UI in Figma, leveraging the newly launched Figma Variables unveiled at Figma Config 2023.
To commence, we constructed a design token dictionary, mandating that all UI designs adhere strictly to these tokens rather than employing arbitrary values.
Setting up a universal Tailwind configuration to integrate the design tokens and generate class names based on these design tokens.
Given that each application already possesses an existing Tailwind configuration, completely removing the entire configuration at once posed a challenge due to the extensive refactoring effort. Instead, we opted to construct a chain of Tailwind configurations. In this setup, each app's Tailwind configuration imports the universal configuration through Tailwind Presets.Leveraging NX Libraries for Code Reusability.
Recognizing the substantial overlap in code for services, presentational UI components, pipes, etc. across each application, we opted to divide them into smaller, independent libraries. we decided to modify the directory structure for adding libs. and here's a glimpse of our directory structure :
.
└── app-v2/
├── apps/
│ ├── accounts/
│ │ ├── src/
│ │ │ └── app/
│ │ │ ├── ...
│ │ │ └── index.html
│ │ └── tailwind.config.js
│ ├── admin/
│ │ ├── src/
│ │ │ └── app/
│ │ │ ├── ...
│ │ │ └── index.html
│ │ └── tailwind.config.js
│ ├── spender/
│ │ ├── src/
│ │ │ └── app/
│ │ │ ├── ...
│ │ │ └── index.html
│ │ └── tailwind.config.js
│ ├── upsert-expe/
│ │ ├── src/
│ │ │ └── app/
│ │ │ ├── ...
│ │ │ └── index.html
│ │ └── tailwind.config.js
│ ├── settings/
│ │ ├── src/
│ │ │ └── app/
│ │ │ ├── ...
│ │ │ └── index.html
│ │ └── tailwind.config.js
│ └── developers/
│ ├── src/
│ │ └── app/
│ │ ├── ...
│ │ └── index.html
│ └── tailwind.config.js
└── libs/
├── accounts/
│ ├── features
│ ├── ui
│ ├── data-access
│ └── utils
├── admin/
│ ├── features
│ ├── ui
│ ├── data-access
│ └── utils
├── upsert-expenses/
│ ├── features
│ ├── ui
│ ├── data-access
│ └── utils
├── developers/
│ ├── features
│ ├── ui
│ ├── data-access
│ └── utils
├── spender/
│ ├── features
│ ├── ui
│ ├── data-access
│ └── utils
├── settings/
│ ├── features
│ ├── ui
│ ├── data-access
│ └── utils
└── shared/
├── features
├── ui
├── data-access
└── utils
We introduced a "libs" folder to centralize all our libraries and specifically established a structured folder arrangement based on the type of library.
As we already have NX in place it already provides a lot of tooling around this setup for the most optimised developer experience.
Everything about Libraries
As per our prior experience, a library is a piece of code that is shared across multiple projects, which is correct but when it comes to developing apps with Nx, there is a bit of misconception, Nx recommends moving most of the code to libraries and using apps as a driver for them.
Within our workspace, there exists a variety of libraries. To uphold a structured environment, we opted for a limited set of types, specifically the following four types of libraries. We also enforced specific naming conventions and dependency constraints.
Feature Library
A feature library is a compilation of files designed to configure a business use case or a specific page in an application. The library primarily consists of smart components that interact with data sources, encompassing a substantial portion of UI logic, form validation code, and related functionalities. These libraries are customized for specific applications, often loaded selectively to optimize efficiency.
Naming Convention
feature (if nested) or feature-* (e.g., feature-my-expenses).
Dependency Constraints
A feature library can depend on any type of library.
When to use Feature Library
A feature library can be used generally to develop a complete feature flow, which includes Smart Components, Presentational Components, Pages, Routing Logic, Injecting Services, etc. It can import any kind of library.
In simple words consider it as a complete module for a feature that previously used to reside under the apps and has been now moved to an independent lib.
UI Library
A UI library comprises a collection of visually interconnected or independent components designed for presentation purposes. Generally, these components do not rely on injected services and instead depend solely on inputs to obtain the necessary data.
Naming Convention
ui
(if nested) or ui-*
(e.g., ui-button
)
Dependency Constraints
A UI library can depend on ui and util libraries.
When to use UI Library
A UI library is typically employed to construct presentational (dumb) components, which can be either singular or composed of multiple dumb components. The essential criterion is that these components should exclusively serve presentation purposes and rely solely on @Inputs
and @Outputs
for their functionality.
Data Access Library
Data-access libraries encompass code serving as intermediary layers on the client side, enabling communication with APIs on the server tier. Furthermore, these libraries include a mechanism for state management.
Naming Convention
data-access
(if nested) or data-access-*
(e.g. data-access-platform-spender-expenses
)
Dependency Constraints
A data-access library can depend on data-access and util libraries.
When to use the Data Access Library
It is generally used to place service and state management utilities.
Utility Library
A utility library consists of fundamental code employed across various libraries. Typically devoid of framework-specific elements, it primarily comprises utilities or pure functions.
Naming Convention
util
(if nested), or util-*
(e.g., util-currency-converter
)
Dependency Constraints
A utility library can depend only on utility libraries.
When to use Utility Library
It is commonly employed when there is a need for something shared across different libraries and possesses a pure nature (e.g., pure functions). Generally, these utilities are advised to be framework-agnostic, although there may be exceptions.
Following conventions and principles:
As part of our restructuring efforts, we aimed for universal adherence to specific naming conventions and directory structures among developers. This can be challenging in a dispersed team, but fortunately, NX provides a useful tool in the form of its generator.
This tool streamlines the process, relieving developers from the burden of memorizing naming conventions or directory structures.
We opted to develop our bespoke Nx Generator designed for library creation. Operating much like a CLI tool (and compatible as a GUI through the Nx console extension in VS Code), this generator simplifies the process by asking three questions and subsequently generates the library with appropriate naming and directory conventions. This eliminates the need for developers to memorize these details. A video showcasing its functionality is attached.
In the demonstration above, we utilized our custom generator by executing the command nx g @fyle/generator:library
. This command triggered three questions:
Library Name.
Library Type.
Library Scope.
The answers to these questions determined the naming convention and the suitable directory for storing the library.
Upon closer inspection, it autonomously appended a "ui-" prefix and established the library within the shared/ui
directory. This alleviates the burden on developers, sparing them from having to make these decisions. Undoubtedly, Nx generators are remarkable tools capable of automating tasks like this, thereby enhancing the Developer Experience (DX).
Up to this point, I've provided you with insights into our ongoing initiatives and approaches. However, there's much more to cover, and it's challenging to encapsulate everything in a single blog post. Therefore, I'll be releasing a series of follow-up articles that delve into specific topics.
These will include discussions on topics such as incorporating Tailwind configuration with design tokens, optimizing structural adherence and enhancing Developer Experience (DX) using NX Generators, enforcing Dependency Constraints, developing our component library, and more.
Keep an eye out for our upcoming articles. If you have any questions or suggestions, feel free to connect with me on LinkedIn.
This is exactly what I was looking for. It's well explained and straight to the point. Fyle is lucky to have such a great engineering team that puts so much effort into the architecture and docs. I'm impressed by this piece of engineering and will suggest this architecture to my tech lead. Thanks again for the article, guys!