<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Enterprise Frontend Workshop]]></title><description><![CDATA[Practical enterprise frontend architecture, built from the ground up. Explore NX monorepos, Angular, Domain-Driven Design, microfrontends, design systems, and A]]></description><link>https://enterprise-frontend-workshop.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6adb625d2c8778db47f3c09f/a5ec4147-3c4b-4f3f-b092-4dadee301d36.png</url><title>Enterprise Frontend Workshop</title><link>https://enterprise-frontend-workshop.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 12:25:37 GMT</lastBuildDate><atom:link href="https://enterprise-frontend-workshop.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Development Tools: WebStorm, Visual Studio Code and OpenCode]]></title><description><![CDATA[Compare WebStorm, VS Code, and OpenCode for Angular, TypeScript, and NX, and choose tools without tying your team to one editor.]]></description><link>https://enterprise-frontend-workshop.hashnode.dev/development-tools-webstorm-visual-studio-code-and-opencode</link><guid isPermaLink="true">https://enterprise-frontend-workshop.hashnode.dev/development-tools-webstorm-visual-studio-code-and-opencode</guid><category><![CDATA[WebStorm]]></category><category><![CDATA[Visual Studio Code]]></category><category><![CDATA[Angular]]></category><category><![CDATA[Nx]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Andrey Pankov]]></dc:creator><pubDate>Thu, 29 Oct 2026 10:40:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/fe32b700-4235-4f9d-bc1c-2277dd6c43f9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2>
<p>When developing frontend applications, the choice of tools affects more than an individual developer’s comfort. It also influences team conventions, development environment setup, version control integration, automation, and the use of AI tools.</p>
<p>This article compares three options for working with TypeScript, Angular, and NX:</p>
<ul>
<li><strong>WebStorm</strong> — a full-featured integrated development environment (IDE).</li>
<li><strong>Visual Studio Code</strong> — an extensible code editor.</li>
<li><strong>OpenCode</strong> — an open-source AI coding agent designed to assist with software development.</li>
</ul>
<p>These tools solve overlapping but different problems. WebStorm and Visual Studio Code provide the primary environment for working with code, while OpenCode focuses on interacting with a codebase through an AI agent. They should therefore not always be treated as directly interchangeable alternatives.</p>
<p>The goal is to help you choose a suitable development tool without making a particular editor a prerequisite for learning NX.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/e1681279-c3c9-400f-9859-cd4e0d8d0946.png" alt="" style="display:block;margin:0 auto" />

<h2>1. WebStorm: A Full-Featured IDE</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/67021b49-0a5d-4d29-8d39-4090b8fb355f.jpg" alt="" style="display:block;margin:0 auto" />

<p>WebStorm is an integrated development environment from JetBrains, focused on JavaScript, TypeScript, and modern frontend technologies.</p>
<p>Unlike a minimalist editor, an IDE aims to provide a cohesive set of tools within a single working environment. These include project navigation, code analysis, refactoring, version control support, run configurations, and integration with development tools.</p>
<h3>Advantages</h3>
<p><strong>Deep project understanding.</strong> The IDE analyses project structure, relationships between files, imports, and types. This helps developers navigate to definitions, find symbol usages, and refactor code with context in mind.</p>
<p><strong>TypeScript and frontend support.</strong> WebStorm provides tools for JavaScript, TypeScript, HTML, CSS, and related technologies. For Angular and other frontend projects, code navigation, type analysis, and integration with familiar development tools are particularly useful.</p>
<p><strong>Git integration.</strong> Reviewing changes and diffs, working with branches, and resolving conflicts are available within the IDE.</p>
<p><strong>A unified working environment.</strong> Many features are available without having to select and coordinate a large collection of extensions yourself.</p>
<p><strong>Support for large projects.</strong> Analysis, search, and refactoring tools can be especially valuable in large codebases, where changing one element may affect several modules.</p>
<h3>Limitations</h3>
<p>The main drawback is its commercial licensing model. Before choosing WebStorm, check the current licence terms and available usage options.</p>
<p>A full-featured IDE may also consume significant system resources. Some capabilities may be unnecessary for smaller projects or developers who prefer a minimalist environment.</p>
<p>WebStorm does not eliminate the need to understand the technologies being used. Automation makes navigating and modifying code easier, but it does not replace architectural decisions or result verification.</p>
<h3>When to choose WebStorm</h3>
<p>Consider WebStorm if:</p>
<ul>
<li>You regularly work with large TypeScript projects.</li>
<li>Advanced navigation and refactoring capabilities are important.</li>
<li>You prefer an integrated environment with a ready-made set of tools.</li>
<li>The licence cost is acceptable for personal or corporate use.</li>
</ul>
<p>Official website: <a href="https://www.jetbrains.com/webstorm/">https://www.jetbrains.com/webstorm/</a></p>
<h2>2. Visual Studio Code: An Extensible Editor</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/18f14ef1-69b3-4052-b42f-928573ea6505.jpg" alt="" style="display:block;margin:0 auto" />

<p>Visual Studio Code (VS Code) is a code editor from Microsoft. It provides core development capabilities that can be extended through its extension system.</p>
<p>VS Code is often chosen for its flexibility, accessibility, and ability to tailor the working environment to a specific project.</p>
<h3>Advantages</h3>
<p><strong>Accessibility.</strong> VS Code is distributed free of charge. However, the terms for the editor itself should be considered separately from those of individual extensions or connected services.</p>
<p><strong>Extensibility.</strong> Capabilities can be added gradually, including Angular support, additional TypeScript tools, Git integration, formatting, linting, and tools for AI-assisted development.</p>
<p><strong>Terminal integration.</strong> The integrated terminal allows developers to run npm, NX CLI, Git, and other commands without constantly switching applications.</p>
<p><strong>Flexible configuration.</strong> A team can agree on a set of extensions, formatting settings, editor rules, and other project-specific options.</p>
<p><strong>Wide adoption.</strong> Many frontend developers are already familiar with VS Code, making it a convenient option for teams with different levels of experience and preferences.</p>
<h3>Limitations</h3>
<p>Flexibility comes with a trade-off: the working environment must be configured and maintained.</p>
<p>If developers use different extensions and settings, formatting, diagnostics, and automation results may vary. Not every extension is equally well maintained, and a large number of additions can make troubleshooting more difficult.</p>
<p>An extension also does not guarantee that all its features are free or that a connected service is available in every region or corporate network.</p>
<h3>When to choose Visual Studio Code</h3>
<p>Consider VS Code if:</p>
<ul>
<li>You need a free editor with broad capabilities.</li>
<li>You want to configure your own development environment.</li>
<li>Your team uses command-line tools and project configuration files.</li>
<li>You need to align the editor with existing development processes.</li>
<li>You prefer to choose AI tools separately from your primary development environment.</li>
</ul>
<p>Official website: <a href="https://code.visualstudio.com/">https://code.visualstudio.com/</a></p>
<h2>3. OpenCode: An AI Agent for Working with Code</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/81a99666-cf98-469c-9fdd-d58dff4f04b4.jpg" alt="" style="display:block;margin:0 auto" />

<p>OpenCode is an open-source AI coding agent designed to assist with software development tasks.</p>
<p>Its primary role differs from that of a traditional editor or IDE. Rather than only providing an interface for manually working with files, an AI agent can analyse project context, help break down a task, suggest changes, and perform permitted actions within its workflow.</p>
<p>OpenCode can be used through a terminal, a standalone application, or integrations with editors and IDEs. The available ways of working depend on the current version of the tool.</p>
<h3>Advantages</h3>
<p><strong>Task context.</strong> You can ask the agent to investigate existing code, explain the project structure, suggest a change plan, or prepare an implementation of a particular feature.</p>
<p><strong>Help with repetitive tasks.</strong> An AI agent can assist with writing tests, preparing documentation, investigating errors, and performing focused refactoring.</p>
<p><strong>Integration with an existing environment.</strong> OpenCode does not have to replace your primary editor. It can be treated as an additional tool within an established development workflow.</p>
<p><strong>Open source.</strong> The ability to inspect and review the tool’s source code may be important when evaluating it and deciding how to use it.</p>
<p><strong>Flexibility in AI infrastructure.</strong> Model and provider integrations depend on the current configuration and supported integrations. Check the available options, costs, and restrictions before using them.</p>
<h3>Limitations</h3>
<p>OpenCode should not automatically be treated as a complete replacement for an IDE. Manual navigation, visual editing, debugging, and other familiar operations depend on the editor or application with which it is used.</p>
<p><strong>Open source does not mean every AI capability is free.</strong> Connecting to models may require paid access through a provider or the use of your own infrastructure.</p>
<p>The availability of specific models and providers may depend on the region, network restrictions, and corporate policies.</p>
<p>Finally, an AI agent may suggest an incorrect solution, modify the wrong files, or misunderstand requirements. Generated code must be reviewed, and changes should be checked using diffs, tests, and other quality checks.</p>
<p>When working with corporate repositories, security rules matter: determine which data may be sent to external services, which tools can execute commands, and which actions require developer confirmation.</p>
<h3>When to choose OpenCode</h3>
<p>Consider OpenCode if:</p>
<ul>
<li>You want to explore AI-assisted development in practice.</li>
<li>You need to delegate limited code analysis or modification tasks to an agent.</li>
<li>You want to use an AI agent independently of a specific editor.</li>
<li>Your team is prepared to review the agent’s output and control its permissions.</li>
<li>You want to evaluate alternative ways of integrating AI into your workflow.</li>
</ul>
<p>Official website: <a href="https://opencode.ai/">https://opencode.ai/</a></p>
<h2>4. Comparing the Tools</h2>
<table>
<thead>
<tr>
<th>Criterion</th>
<th>WebStorm</th>
<th>Visual Studio Code</th>
<th>OpenCode</th>
</tr>
</thead>
<tbody><tr>
<td>Primary purpose</td>
<td>Full-featured IDE</td>
<td>Extensible editor</td>
<td>AI coding agent</td>
</tr>
<tr>
<td>Usage model</td>
<td>Integrated environment</td>
<td>Editor with extensions</td>
<td>Agent-based workflow</td>
</tr>
<tr>
<td>TypeScript work</td>
<td>Built-in IDE tools</td>
<td>Core capabilities and extensions</td>
<td>AI-assisted code analysis and modification</td>
</tr>
<tr>
<td>Git integration</td>
<td>Integrated interface</td>
<td>Editor and extension integration</td>
<td>Depends on the workflow and available tools</td>
</tr>
<tr>
<td>Terminal commands</td>
<td>Supported</td>
<td>Integrated terminal</td>
<td>One of the primary interaction methods</td>
</tr>
<tr>
<td>Configuration</td>
<td>Many capabilities built in</td>
<td>Highly flexible</td>
<td>Depends on configuration and connected providers</td>
</tr>
<tr>
<td>Licensing and cost</td>
<td>Commercial licence; verify current terms</td>
<td>Editor is free; individual services may have their own terms</td>
<td>Open-source tool; model usage may incur costs</td>
</tr>
<tr>
<td>Main trade-off</td>
<td>Cost and resource consumption</td>
<td>Need to select and maintain the environment</td>
<td>Need to verify the agent’s output</td>
</tr>
</tbody></table>
<p>The table shows that the tools differ primarily in their roles. WebStorm and VS Code serve as the main working environment for a project, while OpenCode adds an AI-oriented way to perform engineering tasks.</p>
<p>In some scenarios, they do not compete directly: a developer can use VS Code or WebStorm together with OpenCode.</p>
<h2>5. Choosing Tools for NX and Angular</h2>
<p>When working with NX, it is important to distinguish editing tools from the project’s own tooling.</p>
<p>NX provides its own system for managing workspaces and tasks. Commands for running, building, testing, and other operations are executed through the NX CLI and project configuration. These mechanisms are not exclusive to WebStorm or VS Code.</p>
<p>For example, a developer can open the same NX workspace in different editors, run commands through a terminal, and use Git to track changes. The convenience of integration may differ, but the workspace architecture should not depend on an editor preference.</p>
<p>A starting point could be:</p>
<ul>
<li><strong>WebStorm</strong> — if you want a full-featured IDE with advanced navigation and refactoring.</li>
<li><strong>Visual Studio Code</strong> — if accessibility, flexible configuration, and an extensible environment are important.</li>
<li><strong>OpenCode</strong> — if you want to complement your existing development process with an AI agent.</li>
</ul>
<p>If you are just beginning to learn NX, it makes sense to start with the basic commands and workspace structure. This helps you understand what happens when a task runs, regardless of the editor you use.</p>
<p>An AI agent can help investigate NX configuration or prepare changes, but its recommendations should be checked just like changes proposed by a person.</p>
<h2>6. Practical Recommendations for Teams</h2>
<p>Choosing an editor is an individual decision, but teamwork requires shared conventions.</p>
<h3>Agree on code quality</h3>
<p>Formatting, linting, testing, and builds should rely primarily on project configuration and automated checks rather than individual developer settings.</p>
<h3>Do not tie documentation to a single editor</h3>
<p>If the team uses NX CLI and standard project commands, the instructions remain applicable to developers using different tools.</p>
<h3>Control changes</h3>
<p>Use Git, review diffs, and run the checks required by the project. This is particularly important when using AI agents that can modify multiple files as part of one task.</p>
<h3>Define AI usage rules</h3>
<p>Before connecting AI tools, agree on requirements for confidentiality, source-code access, command execution, and result verification.</p>
<h3>Choose tools for the task</h3>
<p>There is no need to require every developer to use the same editor if the differences do not hinder collaboration. Consistent project rules, reproducible commands, and verifiable quality are far more important.</p>
<h2>Conclusion</h2>
<p>WebStorm, Visual Studio Code, and OpenCode represent different approaches to development.</p>
<p>WebStorm provides an integrated IDE, Visual Studio Code offers a flexible and extensible environment, and OpenCode is an AI agent for carrying out engineering tasks with the help of models.</p>
<p>There is no universal winner. The choice depends on the work involved, budget, developer preferences, corporate restrictions, and requirements for AI tools.</p>
<p>For the Enterprise Frontend Workshop, the main principle remains unchanged: <strong>technical instructions should explain how the project works, not require a particular editor</strong>.</p>
<p>By learning NX CLI, workspace structure, task configuration, and basic Git practices, developers can apply what they learn in their preferred working environment.</p>
<h3>Official Documentation</h3>
<ul>
<li><a href="https://www.jetbrains.com/webstorm/">WebStorm</a></li>
<li><a href="https://www.jetbrains.com/help/webstorm/">WebStorm documentation</a></li>
<li><a href="https://code.visualstudio.com/">Visual Studio Code</a></li>
<li><a href="https://code.visualstudio.com/docs">Visual Studio Code documentation</a></li>
<li><a href="https://opencode.ai/">OpenCode</a></li>
<li><a href="https://nx.dev/">NX documentation</a></li>
</ul>
<p>__</p>
]]></content:encoded></item><item><title><![CDATA[NX Fundamentals — Building the NX Workspace]]></title><description><![CDATA[Build an Angular workspace with NX: organize shared resources and libraries, configure TypeScript aliases, and enforce architectural boundaries.]]></description><link>https://enterprise-frontend-workshop.hashnode.dev/nx-fundamentals-building-the-nx-workspace</link><guid isPermaLink="true">https://enterprise-frontend-workshop.hashnode.dev/nx-fundamentals-building-the-nx-workspace</guid><category><![CDATA[Nx]]></category><category><![CDATA[Angular]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[architecture]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Andrey Pankov]]></dc:creator><pubDate>Thu, 29 Oct 2026 00:21:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/5ecf100d-83d2-40d3-8cc9-d2fae54d1fd4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article continues <a href="https://enterprise-frontend-workshop.hashnode.dev/nx-workspace-from-architecture-to-tooling">NX Workspace — From Architecture to Tooling</a>. We are preparing the workspace's architectural skeleton before implementing the business domain. This is an initial encounter with approaches also used in Domain-Driven Design (DDD), but DDD and its principles will be covered separately in another article. We will not recreate the workspace or the Angular application.</p>
<h2>NX Workspace Concepts and Project Organization</h2>
<p>The previous article explained why an NX Workspace is useful and how to create an Angular application with a separate E2E project. We consider that step complete and continue with the existing workspace.</p>
<p>It is important to distinguish three concepts:</p>
<ul>
<li><strong>Workspace</strong> — the working environment and its shared configuration.</li>
<li><strong>Project</strong> — a unit NX can discover, such as an application, library, or E2E project.</li>
<li><strong>Target</strong> — a named task belonging to a project, such as building, serving, or testing.</li>
</ul>
<p>In this project, <code>personal-finance</code> and <code>personal-finance-e2e</code> are separate NX projects with different responsibilities and available tasks. The set of targets is determined by configuration and plugins; do not assume that every project has the same <code>build</code>, <code>test</code>, and <code>lint</code> targets.</p>
<p>Documentation: <a href="https://nx.dev/reference/project-configuration">NX project configuration</a>.</p>
<h3>The <code>.angular/</code> and <code>.nx/</code> Support Directories</h3>
<p>In addition to <code>apps/</code>, <code>libs/</code>, and configuration files, support directories may appear in a workspace after dependencies are installed and tools are run. They contain caches, intermediate data, or local information used by development tools, but are generally not project source code.</p>
<p><code>.angular/</code> <strong>contains Angular CLI support data.</strong> It may include a build cache that allows results to be reused and subsequent runs to complete faster. Its contents depend on the Angular CLI version and project settings. The directory can usually be deleted: the tools will recreate the required data on the next run. Clearing the cache may make the first subsequent run slower, but should not remove application source code.</p>
<p><code>.nx/</code> <strong>contains NX support data.</strong> Our <code>.gitignore</code> explicitly excludes these subdirectories:</p>
<ul>
<li><code>.nx/cache</code> — cache of NX task results. If a task's inputs have not changed, NX can reuse a previous result instead of running the task again.</li>
<li><code>.nx/workspace-data</code> — local workspace data used by NX to operate and analyze projects.</li>
<li><code>.nx/migrate-runs</code> — data associated with NX migration runs.</li>
</ul>
<p>These directories are excluded from version control because they contain generated or local data that is not needed to restore the source code on another machine. Their contents may differ between developers and environments, and caches can be recreated when needed.</p>
<p>Do not confuse this support data with NX configuration files such as <code>nx.json</code>, <code>project.json</code>, and <code>tsconfig.base.json</code>: configuration defines project behavior and belongs in the repository.</p>
<p>The root <code>.gitignore</code> also excludes:</p>
<ul>
<li><code>.idea/</code> — WebStorm/IntelliJ IDE settings and metadata, which often depend on the machine or developer preferences;</li>
<li><code>node_modules/</code> — installed dependencies restored from package manifests and the package-manager lockfile;</li>
<li><code>dist/</code> — build output that can be regenerated by running the build;</li>
<li><code>__screenshots__/</code> — locally generated screenshots that are not considered source files in this project.</li>
</ul>
<p>The purpose of <code>.gitignore</code> is not to hide inconvenient files; it is to keep generated artifacts and local data out of version history. Source files, settings, the lockfile, and other files required for reproducible team development must remain version-controlled.</p>
<p>See also: <a href="https://angular.dev/cli/cache">Angular CLI cache</a> and <a href="https://nx.dev/features/cache-task-results">NX task caching</a>.</p>
<h2>Organizing <code>apps/</code> and <code>libs/</code>: common and shared</h2>
<p>At this stage, we are not creating a collection of abstract libraries “for the future”. Instead, we establish the minimum structure that will guide the further development of Personal Finance. This requires agreement not only on where applications and libraries live, but also on the purpose of shared resources and NX project tags.</p>
<h3><code>apps/</code> for Applications and <code>common/</code> for Workspace Resources</h3>
<p>The <code>apps/</code> directory contains standalone applications and their associated test projects. Our main Angular application has the NX project name <code>personal-finance</code>. The separate E2E project, <code>personal-finance-e2e</code>, tests user scenarios in the browser. These are two different NX projects, but they belong to the same product.</p>
<p>The <code>common/</code> directory lives <strong>at the workspace root</strong>, alongside <code>apps/</code> and <code>libs/</code>. It does not belong to the application and must not be placed in <code>apps/personal-finance/</code>. It stores shared resources that are connected to the application through its configuration.</p>
<p>Target structure:</p>
<pre><code class="language-text">common/
├── styles/
│   └── common.scss
└── environments/
    ├── common.ts
    ├── environment.dev.ts
    ├── environment.test.ts
    └── environment.prod.ts
apps/
├── personal-finance/
│   ├── project.json
│   └── src/
│       └── app/
└── personal-finance-e2e/
</code></pre>
<p>Shared styles live in <code>common/styles/common.scss</code> and are included separately from the application's own styles. In <code>apps/personal-finance/project.json</code>, list <code>apps/personal-finance/src/styles.scss</code> first and <code>common/styles/common.scss</code> second. This preserves the separation: the first file is responsible for application-local styles, while the second contains workspace-wide styles.</p>
<p>The main application needs three environment modes: <code>dev</code>, <code>test</code>, and <code>prod</code>. Their files are created in <code>common/environments/</code>. This requirement applies to the application, not the E2E project. The mechanism for selecting a file must be explicitly configured in the application's targets/configurations and aligned with the Angular/NX versions in use.</p>
<p>The <code>common/environments/common.ts</code> file exports a shared constant:</p>
<pre><code class="language-typescript">export const COMMON = {};
</code></pre>
<p>Each environment file imports this constant and exports it through the <code>common</code> property while retaining the standard environment properties:</p>
<pre><code class="language-typescript">import { COMMON } from './common';

export const environment = {
  production: false,
  common: COMMON,
};
</code></pre>
<p>This illustrates the configuration shape; it is not the complete content for all three files. Standard property values must match each mode. Create separate files for <code>dev</code>, <code>test</code>, and <code>prod</code> with appropriate values, and configure file selection for serving/building.</p>
<p>Code documentation is mandatory: <strong>all classes and all their public or protected methods and properties must be documented in English</strong>. Comments should explain the purpose of the class or member rather than merely repeat its name.</p>
<h3>Creating <code>common/</code> and Connecting Shared Resources</h3>
<p>We will now move resources to the workspace root so that they are connected through application configuration rather than stored inside the application. Run all commands below from the repository root.</p>
<p>First, create the directories:</p>
<pre><code class="language-bash">mkdir -p common/styles common/assets common/environments
</code></pre>
<p>Keep <code>apps/personal-finance/src/styles.scss</code> where it is: it contains the application's own styles. Create a separate file for workspace-wide styles:</p>
<pre><code class="language-bash">mkdir -p common/styles
touch common/styles/common.scss
</code></pre>
<p>If shared rules already exist, move them into <code>common/styles/common.scss</code> without moving or renaming the application's stylesheet.</p>
<p>There is currently one file in <code>apps/personal-finance/public/</code>: <code>favicon.ico</code>. Move it directly into <code>common/assets/</code>: the favicon is a shared workspace resource that may be used by multiple applications in the future.</p>
<pre><code class="language-bash">mv apps/personal-finance/public/favicon.ico common/assets/favicon.ico
rmdir apps/personal-finance/public
</code></pre>
<p>In <code>apps/personal-finance/src/index.html</code>, find the existing <code>&lt;link rel="icon" ...&gt;</code> element and change its <code>href</code> to the path of the resource at the site root:</p>
<pre><code class="language-html">&lt;link rel="icon" type="image/x-icon" href="favicon.ico"&gt;
</code></pre>
<p>If the original element has additional attributes, keep them and change only <code>href</code>. Since <code>common/assets/</code> will be configured to publish files at the build output root below, <code>favicon.ico</code> will remain available at <code>/favicon.ico</code>.</p>
<p>The resulting shared-resource structure is:</p>
<pre><code class="language-text">common/
├── assets/
│   └── favicon.ico         # Shared workspace favicon
├── styles/
│   └── common.scss         # Shared workspace styles
└── environments/
    ├── common.ts           # Values shared by all environments
    ├── environment.dev.ts
    ├── environment.test.ts
    └── environment.prod.ts
</code></pre>
<p>The <code>common/assets/</code> directory contains static resources connected to the application through <code>assets</code> in its <code>project.json</code>. This is not a TypeScript library: Angular copies these files into the build output, and the application can access them by URL. The shared <code>favicon.ico</code> lives at the root of <code>common/assets/</code> and is published at <code>/favicon.ico</code>; the link in <code>index.html</code> must point to that path.</p>
<h4>Creating Environment Configurations</h4>
<p>Create the shared object and three environment files. The shared object is intentionally empty for now: only settings that genuinely apply to all modes should be added here.</p>
<p><code>common/environments/common.ts</code>:</p>
<pre><code class="language-typescript">export const COMMON = {};
</code></pre>
<p><code>common/environments/environment.dev.ts</code>:</p>
<pre><code class="language-typescript">import { COMMON } from './common';

export const environment = {
  production: false,
  common: COMMON,
};
</code></pre>
<p><code>common/environments/environment.test.ts</code>:</p>
<pre><code class="language-typescript">import { COMMON } from './common';

export const environment = {
  production: false,
  common: COMMON,
};
</code></pre>
<p><code>common/environments/environment.prod.ts</code>:</p>
<pre><code class="language-typescript">import { COMMON } from './common';

export const environment = {
  production: true,
  common: COMMON,
};
</code></pre>
<p>These examples establish a consistent configuration shape. Settings that vary between environments can be added to <code>common</code> later, but do not put secrets here: anything included in a frontend build is accessible to the browser user.</p>
<p>In this approach, <code>environment.prod.ts</code> is the base file imported by the application. The <code>development</code> and <code>test</code> configurations replace it with the corresponding file through <code>fileReplacements</code>. Therefore, application code should import <code>environment</code> from <code>common/environments/environment.prod</code> rather than choosing a file manually at each usage site.</p>
<h4>Updating <code>apps/personal-finance/project.json</code></h4>
<p>Paths to styles and static resources are relative to the workspace root. Configure <code>assets</code> and <code>styles</code> under <code>build.options</code> as follows. Application styles and shared styles are two separate entries, in this order:</p>
<pre><code class="language-json">"assets": [
  {
    "glob": "**/*",
    "input": "common/assets",
    "output": "/"
  }
],
"styles": [
  "apps/personal-finance/src/styles.scss",
  "common/styles/common.scss"
]
</code></pre>
<p><code>output: "/"</code> means that files from <code>common/assets/</code> are published from the site root. For example, <code>common/assets/images/logo.svg</code> will be available at <code>/images/logo.svg</code>, as if it had previously been located in <code>apps/personal-finance/public/images/logo.svg</code>.</p>
<p>Next, add the environments under <code>build.configurations</code>. Keep the existing <code>production</code> and <code>development</code> settings, adding <code>fileReplacements</code> to them. Also add a <code>test</code> configuration:</p>
<pre><code class="language-json">"configurations": {
  "production": {
    "fileReplacements": [],
    "budgets": [
      {
        "type": "initial",
        "maximumWarning": "500kb",
        "maximumError": "1mb"
      },
      {
        "type": "anyComponentStyle",
        "maximumWarning": "4kb",
        "maximumError": "8kb"
      }
    ],
    "outputHashing": "all"
  },
  "development": {
    "fileReplacements": [
      {
        "replace": "common/environments/environment.prod.ts",
        "with": "common/environments/environment.dev.ts"
      }
    ],
    "optimization": false,
    "extractLicenses": false,
    "sourceMap": true
  },
  "test": {
    "fileReplacements": [
      {
        "replace": "common/environments/environment.prod.ts",
        "with": "common/environments/environment.test.ts"
      }
    ],
    "optimization": false,
    "extractLicenses": false,
    "sourceMap": true
  }
}
</code></pre>
<p>No replacement is needed in production mode: the base file is already named <code>environment.prod.ts</code>. The empty <code>fileReplacements</code> array can be omitted; it is shown only for clarity. When inserting this fragment into an existing file, do not create a second <code>configurations</code> key or remove other current project settings.</p>
<p>To make <code>nx serve personal-finance --configuration=test</code> use the new configuration, add this configuration to the <code>serve</code> target:</p>
<pre><code class="language-json">"test": {
  "buildTarget": "personal-finance:build:test"
}
</code></pre>
<p>Keep the existing <code>development</code> and <code>production</code> configurations unchanged. You can then explicitly run the different variants:</p>
<pre><code class="language-bash">npx nx build personal-finance --configuration=development
npx nx build personal-finance --configuration=test
npx nx build personal-finance --configuration=production

npx nx serve personal-finance --configuration=development
npx nx serve personal-finance --configuration=test
npx nx serve personal-finance --configuration=production
</code></pre>
<p>These are application build configurations. The <code>test</code> target that runs unit tests is a separate task and does not automatically become the <code>build:test</code> environment; configure the test runner separately as required by the selected executor.</p>
<p>Verify the resulting configuration and builds:</p>
<pre><code class="language-bash">npx nx show project personal-finance
npx nx build personal-finance --configuration=development
npx nx build personal-finance --configuration=test
npx nx build personal-finance --configuration=production
</code></pre>
<h4>Running the Application with <code>serve</code></h4>
<p>After moving the resources, check the application in the browser as well as checking the build. From the workspace root, run:</p>
<pre><code class="language-bash">npx nx serve personal-finance
</code></pre>
<p>By default, the <code>serve</code> target uses the <code>development</code> configuration. Once the command starts successfully, open the address shown in the terminal (with the current configuration, this is usually <a href="http://localhost:4200">http://localhost:4200</a>).</p>
<p>Specify an environment explicitly when testing a particular configuration:</p>
<pre><code class="language-bash">npx nx serve personal-finance --configuration=development
npx nx serve personal-finance --configuration=test
npx nx serve personal-finance --configuration=production
</code></pre>
<p>Make sure the application loads without errors, global styles are applied, and the favicon is displayed. In the browser developer tools, verify that a request to <code>/favicon.ico</code> returns a successful response. Stop the local server with <code>Ctrl+C</code> in the terminal.</p>
<p>Documentation: <a href="https://angular.dev/reference/configs/workspace-config">Angular workspace configuration — assets, styles, and file replacements</a>, <a href="https://angular.dev/cli/build">Angular build</a>, and <a href="https://nx.dev/docs/reference/project-configuration">NX project configuration</a>.</p>
<h3><code>libs/shared/</code>: Shared Libraries with Explicit Responsibilities</h3>
<p>The <code>libs/</code> directory contains separate NX libraries. At this stage, we will use <code>libs/shared</code> for libraries intended to be used by multiple parts of the application and expected to preserve framework-independent boundaries wherever possible.</p>
<p>The initial set contains four libraries:</p>
<pre><code class="language-text">libs/
└── shared/
    ├── facades/
    ├── dal/
    ├── base/
    └── definitions/
        ├── abstracts/   # Abstract TypeScript classes
        └── classes/     # Regular classes
</code></pre>
<p>These are four independent NX projects, not merely four folders of code. Each project must have its own configuration and explicitly defined tags. Creating the libraries establishes expected responsibility boundaries; it does not mean we must immediately fill each one with many classes. First, create the required structure and minimal entry points, then add implementation as requirements emerge.</p>
<h4>Generating Libraries with NX Commands</h4>
<p>Create the four libraries from the workspace root. In this project, use the <code>@nx/js:lib</code> generator: it is suitable for shared TypeScript libraries that do not need Angular components or other framework-specific elements. The command <code>npx nx g</code> is shorthand for <code>npx nx generate</code>.</p>
<p>During generation, NX may ask which bundler to use to build the library. At this stage, choose <code>none</code>: we do not need a separate build configuration for each internal library while establishing the architectural skeleton and configuring dependencies. This is a deliberate simplification, not a claim that a bundler will never be needed. If we later need to publish a library as a standalone package or build it independently, revisit the choice based on distribution format and project requirements.</p>
<p><strong>A bundler and a test runner are different tools.</strong> Choosing <code>none</code> for library builds does not mean abandoning tests. We plan to use <strong>Vitest</strong> for unit tests, provided the selected Nx version and generator support the required configuration. If the generator asks separately about the test runner, choose Vitest; if it does not, configure testing in a separate step and verify the generated targets and configuration files. Do not assume Vitest is configured automatically just because the library was generated successfully.</p>
<p>Run the commands one at a time and inspect the generated <code>project.json</code> after each generation:</p>
<pre><code class="language-bash">npx nx g @nx/js:lib libs/shared/facades --tags="level:shared,tag:facades"
npx nx g @nx/js:lib libs/shared/dal --tags="level:shared,tag:dal"
npx nx g @nx/js:lib libs/shared/base --tags="level:shared,tag:base"
npx nx g @nx/js:lib libs/shared/definitions --tags="level:shared,tag:definitions"
</code></pre>
<p>If the generator version in use asks for additional parameters, choose settings consistent with the current workspace and verify the result against the rules below. These commands illustrate the intended structure; they do not replace checking the generated files.</p>
<p>Each project must have:</p>
<ul>
<li>an expected NX project name matching the library name;</li>
<li>the tags <code>level:shared</code> and <code>tag:&lt;project-name&gt;</code>;</li>
<li>a public entry point, <code>src/index.ts</code>, for <code>facades</code>, <code>dal</code>, and <code>base</code>;</li>
<li>an alias in the root <code>tsconfig.base.json</code> that follows the repository conventions.</li>
</ul>
<p>For <code>definitions</code>, use category-specific public entry points only: <code>libs/shared/definitions/src/lib/&lt;category&gt;/index.ts</code>. This architecture <strong>must not</strong> have a shared <code>libs/shared/definitions/src/index.ts</code>. This allows individual categories to be imported through separate aliases without creating one barrel for all definitions.</p>
<p>Create seven categories and their <code>index.ts</code> files from the repository root:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

base="libs/shared/definitions/src/lib"
categories=(constants abstracts classes enums types interfaces records)

for category in "${categories[@]}"; do
  mkdir -p "${base}/${category}"
  cat &gt; "${base}/${category}/index.ts" &lt;&lt;EOF
/**
 * Public exports for shared ${category}.
 * Add explicit exports when files are introduced.
 */
export {};
EOF
done
</code></pre>
<p>The script creates category entry points only. When real files are added to a category, replace <code>export {}</code> with explicit exports of existing files, for example <code>export * from './account.type';</code>. Do not use <code>export * from './';</code>: it points back to the same directory and can create a circular reference.</p>
<p>The <code>@nx/js:lib</code> generator may also create starter files named <code>definitions.ts</code> and <code>definitions.spec.ts</code>. They are not needed in this architecture: instead of one starter module, we use categories with separate <code>index.ts</code> files. Delete both generated files completely. If the generator placed them in the standard <code>src/lib/</code> directory, run this from the workspace root:</p>
<pre><code class="language-bash">rm -f libs/shared/definitions/src/lib/definitions.ts
rm -f libs/shared/definitions/src/lib/definitions.spec.ts
</code></pre>
<p>If your generator version created these files elsewhere, remove the generated <code>definitions.ts</code> and <code>definitions.spec.ts</code> inside <code>libs/shared/definitions/</code> at their actual locations. Do not remove the category files <code>src/lib/&lt;category&gt;/index.ts</code>: they are the public entry points.</p>
<p>Verify that seven category entry points exist and that the library has no common entry point:</p>
<pre><code class="language-bash">find libs/shared/definitions/src/lib -maxdepth 2 -type f -name 'index.ts' -print
test ! -f libs/shared/definitions/src/index.ts
</code></pre>
<p>On Windows, use Git Bash or WSL to run this Bash script. The category aliases in <code>tsconfig.base.json</code> must point to <code>libs/shared/definitions/src/lib/&lt;category&gt;/index.ts</code>:</p>
<ul>
<li><code>@workshop:def:consts</code> → <code>libs/shared/definitions/src/lib/constants/index.ts</code></li>
<li><code>@workshop:def:abstracts</code> → <code>libs/shared/definitions/src/lib/abstracts/index.ts</code></li>
<li><code>@workshop:def:classes</code> → <code>libs/shared/definitions/src/lib/classes/index.ts</code></li>
<li><code>@workshop:def:enums</code> → <code>libs/shared/definitions/src/lib/enums/index.ts</code></li>
<li><code>@workshop:def:types</code> → <code>libs/shared/definitions/src/lib/types/index.ts</code></li>
<li><code>@workshop:def:interfaces</code> → <code>libs/shared/definitions/src/lib/interfaces/index.ts</code></li>
<li><code>@workshop:def:records</code> → <code>libs/shared/definitions/src/lib/records/index.ts</code></li>
</ul>
<p>After generation, inspect the discovered projects and graph:</p>
<pre><code class="language-bash">npx nx show projects
npx nx graph
</code></pre>
<p>The generator may update configuration and add files, but it does not know every architectural convention in this repository. Check the names and tags in every <code>project.json</code>, ensure aliases point to existing files, and only then proceed with implementation. Import boundaries are configured separately in the root ESLint configuration through <code>@nx/enforce-module-boundaries</code>.</p>
<p><code>facades</code> <strong>— application services and API facades.</strong> This library contains services through which the application interacts with APIs and uses application operations. A facade may coordinate calls and provide a convenient consumer contract, but it must not contain UI logic specific to a particular frontend framework.</p>
<p><code>dal</code> <strong>— Data Access Layer.</strong> This library handles access to data from different sources: backend APIs and other external or internal sources. It centralizes low-level data retrieval and persistence mechanisms, source-response transformations, and related technical contracts. Data access should be separated from the UI, and transport logic should not be duplicated in components. The exact dependency direction between <code>facades</code> and <code>dal</code> must be established separately when designing the contracts, to avoid cycles.</p>
<p><code>base</code> <strong>— shared foundational functionality.</strong> This library contains fundamental capabilities needed throughout the application that do not belong to one specific feature. Shared helpers for text internationalization are one example. Do not move arbitrary code here merely because it is “shared”; the library's responsibility must remain small and explainable.</p>
<p><code>definitions</code> <strong>— shared definitions and contracts.</strong> This includes constants, types, enums, records, interfaces, facade contract types, and functions independent of the UI and frameworks. The <code>abstracts/</code> directory is for abstract TypeScript classes, while <code>classes/</code> is for regular classes. The library should provide definitions and base classes needed by multiple projects, rather than becoming a place for implementing every service. Contract definitions may be used by both facades and data-access layers, so this library must remain foundational and must not depend on higher-level libraries.</p>
<p>The word “shared” is not, by itself, a sufficient reason to extract code into a library. When choosing a library, understand which responsibility it centralizes, who consumes it, and which dependencies are allowed. If a function belongs to a specific screen or feature, do not move it to <code>libs/shared</code> merely for the sake of reuse.</p>
<h3>Mandatory NX Project Tags</h3>
<p>For the NX graph and future architectural boundary rules to rely on conventions, every project must have tags in its <code>project.json</code>. A directory on disk does not assign tags by itself: verify the actual configuration when creating each project.</p>
<p><strong>Every application project and its associated E2E project</strong> must have the common tag <code>level:app</code> and an individual tag <code>app:&lt;project-name&gt;</code>, where the name comes from the <code>name</code> field in that same <code>project.json</code>. For example, the <code>personal-finance</code> application should have:</p>
<pre><code class="language-json">{
  "name": "personal-finance",
  "projectType": "application",
  "tags": ["level:app", "app:personal-finance"]
}
</code></pre>
<p>For the separate E2E project named <code>personal-finance-e2e</code>, the tags are <code>["level:app", "app:personal-finance-e2e"]</code>. The shared <code>level:app</code> tag allows app-level rules to apply to both the application and E2E project, while <code>app:&lt;project-name&gt;</code> distinguishes them.</p>
<p><strong>Every library inside</strong> <code>libs/shared</code> must have two tags:</p>
<ul>
<li><code>level:shared</code> — indicates that the project belongs to the shared level;</li>
<li><code>tag:&lt;library-name&gt;</code> — an individual tag whose name matches the <code>name</code> field in <code>project.json</code>.</li>
</ul>
<p>For example, the <code>facades</code> library project should contain:</p>
<pre><code class="language-json">{
  "name": "facades",
  "projectType": "library",
  "tags": ["level:shared", "tag:facades"]
}
</code></pre>
<p>The same principle applies to <code>dal</code>, <code>base</code>, and <code>definitions</code>: each library keeps the <code>level:shared</code> tag and adds its own individual tag, respectively <code>tag:dal</code>, <code>tag:base</code>, or <code>tag:definitions</code>. Additional tags may be present if they follow explicitly agreed architectural rules.</p>
<p>Tags only classify projects by themselves. They do not automatically prohibit undesirable dependencies. Permitted import directions and their rationale are defined in the next section; for the restrictions to take effect, configure them in ESLint using NX's <code>@nx/enforce-module-boundaries</code> rule.</p>
<p>Thus, <code>common/</code> at the workspace root stores shared workspace-level resources, while <code>libs/shared/*</code> contains independent libraries with defined responsibilities and tags. This separation allows the structure to evolve without mixing application resources, UI logic, and shared technical contracts.</p>
<h2>NX Module Boundaries: Dependency Rules</h2>
<p>Tags describe the role of each NX project, while <strong>module boundaries</strong> restrict dependency directions between projects. The goal is not to make imports harder, but to preserve architectural boundaries: the application should not bypass facades and access the data layer directly, and foundational libraries must not depend on application-level libraries.</p>
<p>We use the following rules in this workspace:</p>
<table>
<thead>
<tr>
<th>Source project</th>
<th>Allowed dependency tags</th>
<th>Rationale</th>
</tr>
</thead>
<tbody><tr>
<td><code>level:app</code></td>
<td><code>tag:facades</code>, <code>tag:definitions</code>, <code>tag:base</code></td>
<td>The application uses application facades, shared contracts, and foundational capabilities, but does not access the DAL directly.</td>
</tr>
<tr>
<td><code>tag:facades</code></td>
<td><code>tag:dal</code>, <code>tag:base</code>, <code>tag:definitions</code></td>
<td>Facades coordinate application operations, use data access, and rely on shared contracts and foundational functionality.</td>
</tr>
<tr>
<td><code>tag:definitions</code></td>
<td><code>tag:base</code> only</td>
<td>Definitions remain a low-level layer. They must not know about facades or data access.</td>
</tr>
<tr>
<td><code>tag:dal</code></td>
<td><code>tag:base</code>, <code>tag:definitions</code></td>
<td>The data layer can use shared contracts and utilities, but must not depend on application orchestration.</td>
</tr>
<tr>
<td><code>tag:base</code></td>
<td>No workspace projects</td>
<td><code>base</code> is fully isolated: it imports no other workspace projects, existing or future.</td>
</tr>
</tbody></table>
<p>Why this matters:</p>
<ul>
<li><strong>The application depends on facades, not on data-storage implementation.</strong> Otherwise, components and the application begin to know API and data-source details, and changing an implementation affects too much code.</li>
<li><strong>Facades coordinate interaction scenarios.</strong> They can call the DAL and combine operations, hiding data-source details from consumers.</li>
<li><strong>Definitions must not pull in application layers.</strong> Otherwise, shared types and contracts cease to be a stable foundation, circular dependencies may appear, and reuse becomes harder.</li>
<li><strong>The DAL depends on contracts and foundational tools, but not on facades.</strong> This keeps the technical data-access layer unaware of who calls its methods or why.</li>
<li><strong>Base remains a completely isolated foundation.</strong> <code>base</code> must not import any other workspace project—not <code>definitions</code>, <code>dal</code>, <code>facades</code>, applications, or any future project. This is intentionally strict: the foundation must not pull in external contracts, domain details, or application mechanisms. If functionality requires a dependency on another project, place it in another layer instead of weakening this restriction.</li>
</ul>
<p>The restrictions are configured in the root <code>eslint.config.mts</code> using NX's <code>@nx/enforce-module-boundaries</code> rule and the <code>depConstraints</code> array: each <code>sourceTag</code> has an allowed <code>onlyDependOnLibsWithTags</code> list. Invalid dependencies should produce lint errors. After adding or renaming a project, check its tags and run lint to verify that the NX graph and rules classify it as expected.</p>
<h3>A Consistent ESLint Configuration Extension: <code>.mts</code></h3>
<p>In this workspace, we will use the <code>.mts</code> extension for all three ESLint flat configuration files: the root configuration and the application and E2E configurations. The <code>.mts</code> extension denotes a TypeScript module in ES Modules format and is consistent with configurations such as <code>vitest.config.mts</code>. This is a file-format convention; renaming the file alone does not change lint rules.</p>
<p>In ESLint 9, loading a TypeScript <code>.mts</code> configuration in standard Node.js mode requires the <code>jiti</code> package. Install it as a dev dependency:</p>
<pre><code class="language-bash">npm install --save-dev jiti
</code></pre>
<p>Rename all three files so references and conventions remain consistent:</p>
<pre><code class="language-bash">git mv eslint.config.mjs eslint.config.mts
git mv apps/personal-finance/eslint.config.mjs apps/personal-finance/eslint.config.mts
git mv apps/personal-finance-e2e/eslint.config.mjs apps/personal-finance-e2e/eslint.config.mts
</code></pre>
<p>Update the import of the root configuration in both project configurations. In <code>apps/personal-finance/eslint.config.mts</code> and <code>apps/personal-finance-e2e/eslint.config.mts</code>, it must point to <code>../../eslint.config.mts</code>:</p>
<pre><code class="language-typescript">import baseConfig from "../../eslint.config.mts";
</code></pre>
<p>Leave the other rules and imports unchanged. After renaming, verify that the configurations load and the lint targets work:</p>
<pre><code class="language-bash">npx nx lint personal-finance
npx nx lint personal-finance-e2e
</code></pre>
<p>Run the second command if the E2E project has a <code>lint</code> target configured. If ESLint reports that a configuration cannot be found or loaded, check the filenames, relative import paths, presence of <code>jiti</code> in <code>package.json</code> and the lockfile. If the decision is to use <code>.mts</code> throughout the workspace, do not leave some configuration files with the <code>.mjs</code> extension.</p>
<p>The <code>app:&lt;project-name&gt;</code> tag identifies a specific app-related project; it does not replace the common <code>level:app</code> tag. The E2E project gets <code>level:app</code> for workspace-level classification, but its test configuration remains separate from the application's runtime environment settings.</p>
<p>Documentation: <a href="https://nx.dev/features/enforce-module-boundaries">Enforce module boundaries</a>.</p>
<h2>Creating Shared Libraries and TypeScript Path Aliases</h2>
<p>When creating each library in <code>libs/shared/</code>, configure not only its NX project and required tags but also register a public alias in the root <code>tsconfig.base.json</code>. This lets consumers import the library's public API using a stable name instead of long relative paths.</p>
<p>Use separate aliases in the form <code>@workshop:&lt;library-name&gt;</code> for <code>facades</code>, <code>dal</code>, and <code>base</code>; each points to the library's own <code>src/index.ts</code>. For <code>definitions</code>, do not use a shared <code>src/index.ts</code>: each category has its own <code>index.ts</code> under <code>libs/shared/definitions/src/lib/&lt;category&gt;/</code>, and imports use a separate alias <code>@workshop:def:&lt;category&gt;</code>. If the library generator created <code>libs/shared/definitions/src/index.ts</code>, delete that file: the public API of <code>definitions</code> is organized through separate category entry points, so a common barrel is unnecessary. Do not delete the <code>index.ts</code> files inside category directories.</p>
<pre><code class="language-json">{
  "compilerOptions": {
    "paths": {
      "@workshop:facades": ["./libs/shared/facades/src/index.ts"],
      "@workshop:dal": ["./libs/shared/dal/src/index.ts"],
      "@workshop:base": ["./libs/shared/base/src/index.ts"],
      "@workshop:def:consts": ["./libs/shared/definitions/src/lib/constants/index.ts"],
      "@workshop:def:abstracts": ["./libs/shared/definitions/src/lib/abstracts/index.ts"],
      "@workshop:def:classes": ["./libs/shared/definitions/src/lib/classes/index.ts"],
      "@workshop:def:enums": ["./libs/shared/definitions/src/lib/enums/index.ts"],
      "@workshop:def:types": ["./libs/shared/definitions/src/lib/types/index.ts"],
      "@workshop:def:interfaces": ["./libs/shared/definitions/src/lib/interfaces/index.ts"],
      "@workshop:def:records": ["./libs/shared/definitions/src/lib/records/index.ts"],
      "base": ["./libs/shared/base/src/index.ts"],
      "dal": ["./libs/shared/dal/src/index.ts"],
      "facades": ["./libs/shared/facades/src/index.ts"]
    }
  }
}
</code></pre>
<p>Each category alias must point to the public entry point <code>src/lib/&lt;category&gt;/index.ts</code>. The <code>definitions</code> library has no common <code>src/index.ts</code>: consumer code uses the specific <code>@workshop:def:&lt;category&gt;</code> aliases. When adding a category, create its directory and <code>index.ts</code>, add an alias to <code>tsconfig.base.json</code>, and export only the API intended for consumers.</p>
<p>While categories are empty, keep a temporary compilable stub in each category <code>index.ts</code>. Do not use <code>export * from './';</code>: that points to the same directory and can create a circular reference.</p>
<p>For <code>libs/shared/definitions/src/lib/types/index.ts</code>:</p>
<pre><code class="language-typescript">/**
 * Public exports for shared type definitions.
 * Add exports here when types are introduced.
 */
export {};
</code></pre>
<p>Use the same pattern for the other categories, adjusting the comment to reflect the category's purpose:</p>
<ul>
<li><code>constants/index.ts</code> — shared constants;</li>
<li><code>abstracts/index.ts</code> — shared abstract classes;</li>
<li><code>classes/index.ts</code> — shared classes;</li>
<li><code>enums/index.ts</code> — shared enums;</li>
<li><code>types/index.ts</code> — shared type definitions;</li>
<li><code>interfaces/index.ts</code> — shared interfaces;</li>
<li><code>records/index.ts</code> — shared records.</li>
</ul>
<p>When real files are added to a category, replace the temporary stub with explicit exports of existing files, for example <code>export * from './account.type';</code>.</p>
<p><strong>Important:</strong> the <code>definitions</code> library must not contain a common <code>libs/shared/definitions/src/index.ts</code> file. Delete it if the NX generator created one. This rule applies only to the library's root entry point; category <code>index.ts</code> files inside <code>src/lib/&lt;category&gt;/</code> are required and must remain.</p>
<p>Since this workspace does not define <code>baseUrl</code>, all values in <code>paths</code> start with <code>./</code>; otherwise TypeScript may report <code>TS5090</code>.</p>
<p>Example imports:</p>
<pre><code class="language-typescript">import { SomeType } from '@workshop:def:types';
import { SomeConstant } from '@workshop:def:consts';
</code></pre>
<p>Do not import another library's internal files through relative paths: consumers should use its public API. Path aliases provide convenient and stable paths, but <strong>do not replace</strong> module boundaries and do not, by themselves, prohibit forbidden dependencies.</p>
<h2>Project Configuration and Targets</h2>
<p>NX must know which projects belong to the workspace and which operations are available for them. This information is defined by project configuration and the installed tooling. Depending on the plugins in use, some targets may be explicitly defined in configuration files while others may be discovered automatically from project settings.</p>
<p>A <strong>target</strong> is a named task that can be run for a project. For example, a project may provide targets for building, serving, testing, or checking code style. Do not assume that every project has the same set of targets: it depends on the project's purpose and configuration.</p>
<p>You can inspect the resolved configuration of a particular project with NX:</p>
<pre><code class="language-bash">npx nx show project personal-finance
</code></pre>
<p>This command shows how NX sees the <code>personal-finance</code> project, including its available targets. This is more useful than drawing conclusions from filenames or assuming that every project should have <code>build</code>, <code>test</code>, and <code>lint</code> commands.</p>
<p>Do not confuse the workspace-wide configuration with individual project configuration. The <code>nx.json</code> file defines settings that apply to the workspace and NX behavior in general. Project configuration describes a specific project and its tasks; it may be stored in <code>project.json</code> or produced through the configuration-discovery mechanisms used by plugins.</p>
<p>As a result, configuration becomes an interface between the project and development tools. Developers and CI can use it to understand which actions are available, and NX can use it to coordinate their execution.</p>
<p>Documentation: <a href="https://nx.dev/reference/nx-json">NX workspace configuration</a> and <a href="https://nx.dev/reference/project-configuration">NX project configuration</a>.</p>
<h2>How NX Discovers Projects and Runs Tasks</h2>
<p>As the repository grows, it is useful to find out which projects NX actually discovers instead of maintaining a manual list. Run:</p>
<pre><code class="language-bash">npx nx show projects
</code></pre>
<p>The command lists the projects NX recognizes in the current workspace. It is a useful starting point when exploring a repository or after changing configuration. The directory structure helps explain how the code is organized, but the NX project list shows which units it can actually work with.</p>
<p>Once you know a project name, inspect its configuration separately:</p>
<pre><code class="language-bash">npx nx show project personal-finance
</code></pre>
<p>Tasks are run by specifying the project name and target. The general form is:</p>
<pre><code class="language-bash">npx nx run personal-finance:build
</code></pre>
<p>Here, <code>personal-finance</code> is the project name and <code>build</code> is the target. The command works only if that target exists and is configured for the project. Therefore, inspect the available configuration first and then run the required task.</p>
<p>This approach scales with the repository: you do not need to remember every detail of a specific bundler or test tool when a project exposes the corresponding task through NX. NX does not replace the tools themselves; it coordinates their execution based on configuration and installed plugins.</p>
<h2>Serving, Building, Testing, and Linting</h2>
<p>Everyday development uses different checks because each answers a different question:</p>
<ul>
<li><strong>Serving the application</strong> lets you inspect it during development.</li>
<li><strong>Building</strong> checks whether the project can be built with the current configuration.</li>
<li><strong>Unit tests</strong> check individual pieces of behavior.</li>
<li><strong>Linting</strong> detects issues related to static-analysis rules and code style.</li>
<li><strong>E2E tests</strong> check user scenarios through the application and its environment.</li>
</ul>
<p>NX provides a common way to run these actions through project targets. For example, if the project has the corresponding targets, commands may look like this:</p>
<pre><code class="language-bash">npx nx run personal-finance:build
npx nx run personal-finance:test
npx nx run personal-finance:lint
</code></pre>
<p>These examples show the syntax; they do not claim that every project necessarily has all three targets. Determine the available commands from the actual project configuration. E2E checks use a separate project, so first find its exact name and available targets with <code>npx nx show projects</code> and <code>npx nx show project &lt;project-name&gt;</code>.</p>
<h3>Adding Convenient npm Scripts to <code>package.json</code></h3>
<p>NX commands can be run directly with <code>npx nx</code>, but it is often convenient to give frequently used operations short names in the root <code>package.json</code>. This provides a single entry point for developers and CI: the invocation details live in the repository rather than in personal notes.</p>
<p>Add a <code>scripts</code> object to the root <code>package.json</code>, or extend the existing one without removing other scripts:</p>
<pre><code class="language-json">{
  "scripts": {
    "serve": "nx serve personal-finance",
    "build": "nx build personal-finance",
    "build:all": "nx run-many -t build",
    "build:prod": "nx build personal-finance --configuration=production",
    "test": "nx run-many -t test",
    "lint": "nx run-many -t lint",
    "nx:graph": "nx graph",
    "affected": "nx affected -t lint test build"
  }
}
</code></pre>
<p>What the commands do:</p>
<ul>
<li><code>npm run serve</code> — serves the main Personal Finance application in development mode.</li>
<li><code>npm run build</code> — builds the <code>personal-finance</code> application.</li>
<li><code>npm run build:all</code> — runs the <code>build</code> target for projects that have it configured; it does not assume every project has a build.</li>
<li><code>npm run build:prod</code> — builds the application with the production configuration.</li>
<li><code>npm test</code> — runs <code>test</code> targets for projects where they are available.</li>
<li><code>npm run lint</code> — runs lint for projects that have the corresponding target configured.</li>
<li><code>npm run nx:graph</code> — opens the project graph visualization.</li>
<li><code>npm run affected</code> — runs lint, tests, and builds for projects NX considers affected by changes.</li>
</ul>
<p>The <code>build:all</code>, <code>test</code>, and <code>lint</code> scripts use <code>nx run-many</code> because their purpose is to run the same task across multiple projects. The <code>build</code> script is intentionally limited to the main application. To check only one project, keep using targeted commands such as <code>npx nx lint personal-finance</code>.</p>
<p>The <code>affected</code> command depends on the Git comparison range. For local runs, ensure the base revision is available and appropriate for the task; in CI, the target branch's base revision and the current revision are usually specified explicitly. For example:</p>
<pre><code class="language-bash">npx nx affected -t lint test build --base=origin/main --head=HEAD
</code></pre>
<p>Replace <code>origin/main</code> if your workflow uses a different target branch. You can add a separate npm script with a fixed base branch, but avoid hard-coding one if local and CI workflows use different bases.</p>
<p>After adding the scripts, verify that their names match the actual project targets. If a target is missing, NX cannot run that task for the project; first inspect the configuration with <code>npx nx show project &lt;project-name&gt;</code>.</p>
<p>The point of this section is not to repeat the command list from the previous article. The important thing is to understand the NX model: a command addresses a specific project and target, and NX runs the corresponding task while accounting for configuration and dependencies.</p>
<p>This provides a consistent interface for local development and automation. However, a shared command does not guarantee test quality, correct linting, or comprehensive checks. Their purpose and quality still depend on configuration and the team's engineering decisions.</p>
<h2>The Project Graph and the Task Graph</h2>
<p>As a workspace grows, it becomes increasingly important to understand not only which projects exist but also how they are connected. NX builds a <strong>project graph</strong>, representing discovered dependencies between projects. Inspect it with:</p>
<pre><code class="language-bash">npx nx graph
</code></pre>
<p>The graph helps explore the workspace and answer practical questions: which projects use this library? Which parts of the system may be affected by a change to shared code? Has one library become a dependency of too much of the application?</p>
<p>The graph itself does not tell us whether the architecture is good. It shows discovered dependencies, but cannot automatically decide whether a particular relationship makes sense in terms of business responsibility. That requires architectural agreements and analysis of why the projects interact.</p>
<p>The <strong>task graph</strong> answers a different question: which operations must run, and in what order? One task, for example, may depend on another finishing first. NX can take project relationships and configured task dependencies into account when planning execution.</p>
<p>Thus, the project graph describes relationships between projects, while the task graph describes relationships between operations to be performed. The graphs are related, but they are not interchangeable.</p>
<p>The distinction becomes especially important in large repositories. A change to a shared library may affect several applications, while running a task for one application may require prerequisite operations. Graphs make these relationships explicit instead of relying solely on individual developers' knowledge.</p>
<p>See: <a href="https://nx.dev/features/explore-graph">Explore the project graph</a>.</p>
<h2>Caching and Affected Commands</h2>
<p>As a workspace grows, rerunning every check after each change can take increasingly long. NX supports <strong>task caching</strong>: when a task is configured for caching and its inputs match a previously computed result, NX can reuse that result instead of executing the task again.</p>
<p>Caching does not mean that every command automatically becomes faster. To produce a correct result, NX must account for the data that influences the task: source files, configuration, dependencies, and other configured inputs. If a task depends on external state or performs side effects, its suitability for caching must be evaluated separately. An incomplete description of inputs can make result reuse incorrect.</p>
<p>Another capability is to run tasks only for projects affected by changes. <strong>Affected</strong> commands identify the relevant set of projects based on changes and the dependency graph. This is especially useful in CI, where running every check across the entire repository can be expensive.</p>
<p>The general command form is:</p>
<pre><code class="language-bash">npx nx affected -t lint test build --base=&lt;base-revision&gt; --head=&lt;current-revision&gt;
</code></pre>
<p>The values <code>&lt;base-revision&gt;</code> and <code>&lt;current-revision&gt;</code> are placeholders that must be replaced with real Git revisions appropriate for the scenario. In CI, these are often the target branch's base revision and the revision being checked. Do not blindly copy the same pair into every workflow: the base depends on what you want to compare.</p>
<p>Affected commands do not replace an understanding of dependencies either. Their effectiveness depends on correct project discovery, the graph, and task configuration. Before claiming that CI time has been reduced, configure the scenario for the repository and measure the result.</p>
<p>Caching and affected solve different problems: caching reuses a task result when inputs match; affected identifies which tasks should be considered for the changed part of the workspace. Together they can reduce unnecessary work, but do not remove the need to configure and verify the process properly.</p>
<p>Documentation: <a href="https://nx.dev/features/cache-task-results">Task caching</a> and <a href="https://nx.dev/ci/features/affected">Affected commands</a>.</p>
<h2>Early Architectural Decisions Without Premature Abstraction</h2>
<p>NX provides mechanisms for organizing a workspace, but it does not choose the architecture for the team. Directory structure and the dependency graph show how code is technically organized; they do not prove that responsibilities are divided correctly.</p>
<p>Before creating a library, identify the stable responsibility it will isolate and the problem a separate boundary will solve. Sometimes a structure within the application is sufficient; sometimes a library helps control dependencies, reuse code, or define a public contract. The decision should depend on how the code changes and who uses it, not on a desire to create familiar <code>domain</code>, <code>data-access</code>, <code>ui</code>, or <code>shared</code> folders in advance.</p>
<p>A practical principle is: identify the problem first, choose the boundary next, and only then enforce it with NX rules. Tags and <code>@nx/enforce-module-boundaries</code> are useful when they protect a deliberate architectural decision; by themselves, they do not replace that decision.</p>
<p>Documentation: <a href="https://nx.dev/features/enforce-module-boundaries">Enforce module boundaries</a>.</p>
<h2>Scope of This Stage: Preparing the Skeleton</h2>
<p>At this stage, I usually finish preparing the project's skeleton: the workspace, core shared libraries, naming conventions, path aliases, and strict dependency boundaries. This provides a predictable foundation for further development, but it is not yet an implementation of the business domain.</p>
<p>Structural separation into layers and explicit dependency rules already overlap to some extent with architectural ideas found in Domain-Driven Design. However, we are not designing a domain model, bounded contexts, aggregates, or domain events here. <strong>DDD will be covered separately in the next dedicated article</strong>, so that preparing the technical skeleton is not mixed with domain modeling.</p>
<h2>Current Implementation Status</h2>
<p>This is the second article in the series. It continues from the workspace and Angular application established in the previous article, <a href="https://enterprise-frontend-workshop.hashnode.dev/nx-workspace-from-architecture-to-tooling">NX Workspace — From Architecture to Tooling</a>.</p>
<p>The current workspace already contains the Personal Finance application and a separate E2E project. We do not recreate them: we use the existing implementation to configure shared resources, shared libraries, aliases, and dependency constraints.</p>
<p>At this stage, the domain model and business functionality are not yet being implemented. DDD and domain modeling remain a separate topic; here we are preparing the technical foundation for further work.</p>
<h2>Further Reading</h2>
<ul>
<li><a href="https://nx.dev/">Official NX documentation</a></li>
<li><a href="https://nx.dev/reference/nx-json">NX workspace configuration</a></li>
<li><a href="https://nx.dev/reference/project-configuration">NX project configuration</a></li>
<li><a href="https://nx.dev/features/explore-graph">NX project graph</a></li>
<li><a href="https://nx.dev/features/cache-task-results">Task caching</a></li>
<li><a href="https://nx.dev/ci/features/affected">Affected commands</a></li>
<li><a href="https://nx.dev/features/enforce-module-boundaries">Module boundary rules</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Architecture in Practice: Decisions Behind Enterprise Frontend Systems]]></title><description><![CDATA[Explore frontend architecture decisions involving legacy systems, multiple frameworks, secure configuration, and maintainable enterprise workspaces.]]></description><link>https://enterprise-frontend-workshop.hashnode.dev/architecture-in-practice-decisions-behind-enterprise-frontend-systems</link><guid isPermaLink="true">https://enterprise-frontend-workshop.hashnode.dev/architecture-in-practice-decisions-behind-enterprise-frontend-systems</guid><category><![CDATA[software architecture]]></category><category><![CDATA[frontend]]></category><category><![CDATA[System Design]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Andrey Pankov]]></dc:creator><pubDate>Tue, 27 Oct 2026 15:27:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/e399b9b9-ddb6-4794-907b-b2acdcd0d185.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This article opens the theoretical thread of the Enterprise Frontend Workshop. It explores real architectural dilemmas and why different projects arrive at different solutions. The next step is to translate these ideas into the technical organization of an NX workspace.</em></p>
<h2>Architecture Before Technology</h2>
<p>When a new project begins, it is easy to become absorbed in choosing technologies. Which frontend framework should be used? How should the frontend communicate with the backend? What project structure will make development comfortable while allowing the application to evolve? Should reuse be designed in from the beginning, or should the team focus first on delivering the requirements?</p>
<p>These questions tend to arrive one after another as the team tries to work out where to start. But technology choices rarely exist in isolation. They have to account for systems already in use, the people who depend on them, and changes that may become necessary a year or several years later.</p>
<p>At the beginning, everything can seem straightforward. The requirements are known, the main user journey is clear, and the timeline and constraints are understood. The team can choose a framework, create an application, and start building. Enterprise products, however, rarely remain exactly as they were originally conceived. Requirements evolve, other teams become involved, additional applications appear, and the ways people interact with the product change. Sometimes the new solution must integrate with systems that have existed for many years and are still doing their job.</p>
<p>At some point, the task turns out to be more than building a new application. The application has to fit into an existing environment, preserve working processes, account for the team's experience, and leave room for future development.</p>
<p>That is when it becomes clear that choosing a technology is only part of the work. It is just as important to decide what can remain as it is, what must be connected, and what consequences today's choices may have for later stages of the project.</p>
<p>Architecture is more than the structure of source code or a way of splitting an application into modules. It determines how parts of a system interact, which decisions can change independently, where responsibilities begin and end, and how expensive future changes are likely to be.</p>
<p>Architecture also does not exist separately from the organization the software serves. The same technical solution may work well in one project and poorly in another. Much depends on the problems to be solved, the systems already in place, the team's experience, and what happens if the original assumptions change.</p>
<p>Before discussing specific tools, it is therefore useful to understand where architectural decisions come from. Why do we sometimes have to combine technologies that appear incompatible at first? Why might one product contain several frontend frameworks? Why can a decision that initially feels convenient require a serious rethink a few years later? And why is the simplest technical solution not always the most economical one for an organization?</p>
<p>The answers are best found not in abstract rules, but in situations that actually arise when enterprise systems are built and maintained.</p>
<h2>When New Functionality Should Not Disrupt How People Work</h2>
<p>Consider a common situation.</p>
<p>An organization has been using SharePoint for years. For its employees, it is a familiar working environment: they work with spreadsheets and documents, access data, perform routine operations, and follow established processes. They know where information is stored and how to get their work done. They do not have to think about the system's internals every day; they simply use it to handle everyday tasks.</p>
<p>At some point, the organization needs a mobile application for iOS and Android. The application should provide additional capabilities, work with corporate data, and support the required user journeys.</p>
<p>The existing employee workflow, however, should not change. People continue working in SharePoint as before. They should not have to move to another system, retrain, or reorganize their routines just because a new application has been built elsewhere. The mobile app can offer an additional way to interact with the corporate system without disrupting the familiar environment of people who do not need it.</p>
<p>At first glance, this may look like a purely technical task: build an interface, retrieve data, connect to corporate infrastructure, and configure notifications. But before choosing the technologies, the team has to decide how to add new capabilities without disturbing what already works.</p>
<p>One alternative would be to build a new system, migrate the data, and gradually change the existing processes. Sometimes that is necessary. If the old platform no longer meets business needs, replacing it may be justified. But if the existing environment is still doing its job, a reasonable question arises: does it really have to be replaced just to give users additional capabilities?</p>
<p>Such a transition would involve more than development. It would require data migration, employee training, changes to working procedures, and time for people to adapt. The organization has already invested in the current infrastructure, and employees are accustomed to the way it works.</p>
<p>In this case, the more important goal was not to replace the familiar environment, but to extend the existing system so that people could continue working without unnecessary disruption.</p>
<p>The implementation was far from trivial. The solution combined SharePoint, Microsoft Azure services, Azure WebJobs, and Firebase. Each component had its own role: the existing corporate environment remained in use for working with data, cloud mechanisms handled the necessary background operations and event processing, and Firebase was used to send push notifications to mobile applications.</p>
<p>The result was a system that combined technologies from different platforms. Viewed only through the lens of technological uniformity, one might ask why everything was not built on a single stack and according to one consistent pattern.</p>
<p>But the diversity was not introduced for its own sake. It reflected the project's real constraints. The existing infrastructure was already doing useful work, the mobile application needed additional capabilities, and users did not need to change their familiar way of working with the system.</p>
<p>Each component addressed a particular need, while the architecture connected them into one functioning solution.</p>
<p>This does not mean that combining platforms is always better than using a single system. Integration takes time, knowledge, and ongoing maintenance. Sometimes the number of connections between services grows so large that replacing part of the infrastructure becomes more economical. That conclusion, however, should follow from an assessment of the actual situation—not from an assumption that uniformity is inherently the main sign of good architecture.</p>
<p>In this example, the economic benefit had several sources. There was no need to disrupt employees' everyday work, rebuild functionality already provided by the existing infrastructure, or move every process to a new system just to introduce a mobile app. Development could focus on the capabilities that were actually missing.</p>
<p>Keeping the familiar environment also reduced organizational costs associated with changing workflows. Even if a new system is technically more convenient, moving to it can require considerable effort from people who rely on software to do their jobs every day.</p>
<p>The complex implementation therefore solved a relatively simple problem from the user's perspective: the system gained new capabilities while everyday work continued in the familiar way.</p>
<p>For employees, success was not measured by the number of integrations or by how unusual the technology stack looked. What mattered was that their work did not stop or become harder, even though new functionality had been added behind the scenes.</p>
<p>This example shows why an architectural decision cannot be judged solely by counting technologies, components, or integrations. We need to understand what they do, why those interaction patterns were chosen, and what would change if a different approach were used.</p>
<p>Sometimes architecture becomes simpler by reducing the number of components. In other cases, several technologies make it possible to preserve working systems and avoid much more expensive changes.</p>
<p>The important thing is to understand why a particular option fits the organization and what consequences it creates—not only today, but also in the future.</p>
<h2>When One Product Evolves Across Several Frontend Frameworks</h2>
<p>A similar situation can arise within the frontend product itself, even when the issue is not corporate infrastructure but applications developed by different teams.</p>
<p>Imagine a large system whose main application is written in Angular. It has existed for several years, contains many features and integrations, and its team understands how it works. Over time, a development model has formed around it, useful solutions have accumulated, and dependencies on other parts of the product have emerged.</p>
<p>Then a new functional area is built with React. Some time later, another team starts a separate part of the product in Vue. In parallel, a mobile application becomes necessary, and Ionic or React Native is considered.</p>
<p>The product now contains different frontend frameworks.</p>
<p>Why does this happen? There can be many reasons. Applications may have been developed at different times, by different teams, and for different user journeys. Teams may have different experience. A new part of the product may have grown out of an existing application that already used a particular stack. An organization may also bring together several products, each with its own history and technical constraints.</p>
<p>This situation does not automatically mean that someone made a mistake at the beginning. Technological diversity often emerges as a consequence of business growth, changing requirements, and new needs.</p>
<p>Of course, if a product is just starting, all its applications have similar requirements, and one team is building them, using a single framework is often sensible. It makes UI components easier to reuse, reduces the number of tools, and simplifies developer onboarding. Shared conventions are also easier to maintain when applications follow similar patterns.</p>
<p>But suppose the applications already exist, are actively used, and contain a substantial amount of functionality. Bringing everything onto one framework is no longer a matter of changing a few dependencies. It becomes a migration program that may consume significant resources.</p>
<p>If the main Angular application is stable and meets business needs, rewriting it in React means rebuilding interfaces, retesting business journeys, preserving integrations, and verifying that the new version behaves like the old one. Even if development goes well, the team spends time restoring existing capabilities instead of delivering new ones.</p>
<p>There are also costs for testing, the transition period, training, and fixing regressions. If the old and new versions have to be supported at the same time, the expense grows further.</p>
<p>Before deciding to standardize on one technology, the team should therefore ask: what specific problem is this change supposed to solve?</p>
<p>If maintaining several frameworks genuinely impedes product development, creates substantial costs, or prevents effective sharing of common components, moving to one platform may be justified. But if the applications continue to do their jobs and their differences do not create critical problems, rewriting them may be an unjustifiably expensive way to achieve uniformity.</p>
<p>A different task then emerges: preserve the ability to evolve applications built with different technologies without letting the product become a collection of independent systems that solve the same problems in incompatible ways.</p>
<p>This requires distinguishing two concepts: technological uniformity and architectural consistency.</p>
<p>Several applications can use different frameworks while following common rules for organizing the system. They may have their own entry points, UI construction techniques, and build mechanisms, while still sharing selected parts of their functionality.</p>
<p>For example, different applications may use the same data models, share contracts for backend communication, and run the same data-processing algorithms. If these parts do not depend on a particular UI framework, they can be organized so that they do not need to be implemented separately for every application.</p>
<p>That does not mean trying to share absolutely everything. An Angular component does not become usable in React simply because both applications use TypeScript. A service built on Angular mechanisms is not automatically framework-independent either. Different technologies have their own models for component interaction, state management, and UI construction.</p>
<p>Shared code should therefore be limited to parts that genuinely solve the same problem and can exist independently of a specific application's framework. UI-specific code and technology-dependent mechanisms can remain specific to their respective frameworks.</p>
<p>This approach makes it possible to preserve existing applications, make use of the experience accumulated by the teams, and continue developing the product together.</p>
<p>Technological diversity then stops being an automatic sign of chaos. It becomes a characteristic of the system that can be managed when responsibilities and interaction rules are explicit.</p>
<p>Of course, multiple frameworks introduce costs. Teams have to maintain different tools, account for differences between technology environments, and sometimes build separate versions of UI components. The goal is not simply to preserve freedom of choice, but to understand when that freedom creates real value.</p>
<p>Architecture does not mean allowing every technology without restriction. It helps determine where differences are justified, where a common approach is needed, and which decisions will let the product evolve without constantly reworking applications that already work.</p>
<h2>When a Convenient Configuration Mechanism Needs to Be Reconsidered</h2>
<p>Another example concerns application configuration. It shows how an architectural decision can be reasonable when first introduced, yet require reconsideration later as the project changes and the mechanism begins to be used in ways its designers did not intend.</p>
<p>In 2017, a long-running project based on Angular 2 began. There was a need to change certain settings without recompiling the application. An external JSON configuration file hosted on a web server was introduced for that purpose.</p>
<p>For the original requirement, this was convenient. When a public setting needs to change, rebuilding and releasing the entire application is not always desirable. External configuration separates selected settings from the main code and allows them to change independently of a release.</p>
<p>The original intention was not to store sensitive data in this configuration. The mechanism was meant for a defined set of settings and solved a specific problem.</p>
<p>But the project continued to evolve. Requirements changed, developers moved between tasks, and existing solutions remained in use. Gradually, the JSON configuration began to be used more broadly than intended, and sensitive data started appearing in it.</p>
<p>A convenient mechanism had ended up being used for a purpose it was not designed to serve.</p>
<p>The problem was not that JSON is inherently a bad configuration format. The problem was that data that should not be available to a client-application user was being placed in a publicly accessible file.</p>
<p>Once this was identified, the unsafe practice had to stop and the configuration approach had to change. Using the mechanism in that way was prohibited, and a different organization of configuration became a required decision despite the conflict this caused.</p>
<p>The general idea of shared configuration remained useful. Common parameters still needed to be defined in one place, with overrides for particular environments. What changed was not the principle of centralized configuration management, but its implementation and the constraints on how it could be used.</p>
<p>It is important not to conclude that TypeScript automatically provides security. If sensitive data is included in a client-side bundle, it does not become protected because it was written in TypeScript, minified, or obfuscated. Secrets must be stored and used on a protected side; the client application should receive only values that are safe to expose to its users.</p>
<p>Why does this example matter architecturally?</p>
<p>It shows the difference between the original intention and the life of a solution inside a long-running project. When it was created, the mechanism met the requirement. Later, it began to be used differently, changing the assessment of whether that approach was acceptable.</p>
<p>In enterprise development, it is impossible to assume that every decision will be used only under the conditions that existed when it was introduced. A project may evolve for years. People, requirements, and code usage patterns change. It is therefore important not only to create a convenient mechanism, but also to define the boundaries within which its use remains acceptable.</p>
<p>Sometimes those boundaries can be established through documentation and team agreements. In other cases, the approach itself must change to make undesirable use less likely. And when security is involved, convenience and familiarity are not enough reasons to keep using an unsafe solution.</p>
<p>This case also shows that architectural responsibility is not limited to technical implementation. Sometimes it requires organizational decisions, explaining why they are necessary, and changing established practices.</p>
<p>Architecture, in this sense, is not an attempt to preserve the original design at any cost. It is the willingness to revisit decisions as circumstances change and to keep the system aligned with current requirements.</p>
<h2>When Different Technologies Come Together to Solve One Problem</h2>
<p>The situations discussed so far show that architectural decisions can arise for different reasons. Sometimes a familiar working environment must be preserved. Sometimes several applications need to evolve across different frontend frameworks. Sometimes a convenient mechanism must be reconsidered because security requirements have changed.</p>
<p>A broader question remains: how should we assess solutions that appear technically complex or inconsistent at first?</p>
<p>Suppose one option is to build the system on a single platform, but doing so requires replacing existing components, hiring additional specialists, and changing established processes. Another option uses several technologies, each responsible for a specific task, but requires the team to manage their interactions.</p>
<p>Which option is better?</p>
<p>Without context, there is no way to answer. If multiple technologies create complex dependencies, require scarce expertise, and increase maintenance costs, a single platform may be preferable. But if existing systems continue to work and migration would be expensive without bringing significant benefits to users, preserving the current solution may be more reasonable.</p>
<p>The same applies to frontend framework choices. A single framework can simplify development and maintenance. Yet rewriting several large applications solely for uniformity can consume resources that would be better spent improving functionality.</p>
<p>The economic impact of an architectural decision has many components. It includes not only implementation time, but also maintenance, staff training, use of existing infrastructure, change risk, and the effect on users' everyday work.</p>
<p>Architecture is therefore more closely connected to economics than it may initially appear. It determines not only how software is structured, but also how much effort an organization will spend developing it over time.</p>
<p>There is no universal rule that can identify the right option for every situation in advance. The decision depends on requirements, constraints, and the consequences of each alternative.</p>
<p>Sometimes a more complex technical arrangement is economically preferable because it preserves working infrastructure. Sometimes standardization really does reduce costs. Sometimes shared functionality should be extracted; in other cases, two independent implementations are simpler.</p>
<p>In practice, it is more useful to compare real options than to look for a diagram that appears flawless: what must change, what can remain, who will maintain the solution, and what price the organization will pay for each path.</p>
<h2>Why Architecture Must Be Preserved as the Product Evolves</h2>
<p>Even well-considered initial decisions do not guarantee that a system will retain its shape as it grows.</p>
<p>Development consists of many small changes. Each task requires a solution that meets the requirements within the available time. Sometimes it is easier for a developer to use a neighboring module directly than to find or create an appropriate shared library. Sometimes duplicating a small piece of code appears faster than changing the existing structure.</p>
<p>An individual shortcut may be justified. But when such shortcuts accumulate, the boundaries between parts of the system begin to blur. Libraries acquire dependencies on applications, shared logic becomes mixed with framework-specific code, and a change in one place unexpectedly affects another.</p>
<p>The system becomes more difficult not necessarily because it contains more code, but because developers find it harder to understand the consequences of a change.</p>
<p>Architectural agreements therefore need to be more than written down; they must be maintained during development. The team should understand why particular boundaries exist, which dependencies are allowed, and when existing decisions should be reconsidered.</p>
<p>Some of this work happens through architectural discussions, the involvement of experienced developers, and code review. Other rules can be encoded so that unwanted dependencies are detected automatically.</p>
<p>This is especially important in large products with multiple applications and many libraries evolving at once. Without clear rules, a shared codebase can gradually become a collection of projects that happen to sit next to each other but are connected in arbitrary ways.</p>
<p>Automated checks do not replace architectural thinking. They cannot decide on their own which business capabilities should be grouped together, whether one framework should be used, or where the boundary between two responsibilities belongs. But they can help preserve decisions that have already been made and catch some violations before they become part of the main codebase.</p>
<p>Architecture is therefore not only an initial design. It is also the ability to maintain a coherent system as it evolves, revisit outdated decisions in time, and keep the product manageable.</p>
<h2>From Architectural Thinking to Project Organization</h2>
<p>All the examples share one pattern: architectural decisions arise from the need to reconcile different aspects of a product.</p>
<p>In one case, mobile functionality must be added while preserving employees' familiar environment and the existing infrastructure. In another, several frontend applications need to evolve across different frameworks without rewriting a working product just to achieve uniformity. In a third, a convenient configuration mechanism must be reconsidered after its use changes and a security issue emerges.</p>
<p>Every case requires attention not only to code, but also to users, existing systems, the team's experience, the cost of change, and the product's future.</p>
<p>That is why architecture starts with understanding the problem and its constraints, not with choosing a tool. First, we need to identify which parts of the system must interact, which decisions can evolve independently, and which dependencies may cause problems later. Only then does it make sense to choose how code should be organized and how applications should communicate.</p>
<p>As a product grows, this approach becomes particularly important. When several applications, shared libraries, and technology environments coexist, it is no longer enough to agree on the structure of each project separately. The team needs to maintain a shared view of the system and ensure that local changes do not undermine its architectural boundaries.</p>
<p>This is where tools that organize multiple applications and libraries within one development workspace become useful. They help manage relationships between projects and maintain established rules.</p>
<p>But tools do not make architectural decisions for the team. They do not know why combining frameworks is justified in one product, why an existing system should be preserved, or which configuration values must remain shared. Those decisions come from understanding the actual problem and the consequences of the available alternatives.</p>
<p>Before moving to specific tools and project structures, it is therefore important to understand the reasoning behind architectural decisions: which problems we are trying to solve, why some approaches fit better than others, and which principles should remain valid regardless of the technologies used.</p>
<p>The point of architecture is not to make every part of a system identical. It is to understand why the parts are structured as they are, which differences are justified, and what must be protected from accidental change as the product continues to grow.</p>
<p>Only then does it make sense to explore how those decisions can be expressed in the project structure and maintained throughout the product's lifecycle.</p>
]]></content:encoded></item><item><title><![CDATA[NX Workspace: From Architecture to Tooling]]></title><description><![CDATA[A practical guide to NX Workspace: setting up Angular, configuring testing and linting, and validating a modern frontend toolchain.]]></description><link>https://enterprise-frontend-workshop.hashnode.dev/nx-workspace-from-architecture-to-tooling</link><guid isPermaLink="true">https://enterprise-frontend-workshop.hashnode.dev/nx-workspace-from-architecture-to-tooling</guid><category><![CDATA[Angular]]></category><category><![CDATA[Nx]]></category><category><![CDATA[monorepo]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Andrey Pankov]]></dc:creator><pubDate>Mon, 26 Oct 2026 15:19:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/c4c602e4-87bb-4ea8-9e43-9a4e83d317b4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What Is NX and Why Use It?</h2>
<p>NX is a set of tools for developing, building, and maintaining applications and libraries. For me, its main advantage is not simply the ability to keep several projects in one repository, but the tools it provides to manage how those projects interact and to enforce architectural boundaries.</p>
<h2>Monorepos and NX Workspace</h2>
<p>As an application evolves, other applications and libraries may appear alongside it, often sharing code. One approach is to use a monorepo, where projects are stored together while retaining their individual purposes.</p>
<p>A repository alone, however, is not enough. We need to define the project structure, how projects interact, and which dependencies are allowed. An NX Workspace provides the workspace and project-management tools to do this.</p>
<p>NX does not define the architecture for us. The workspace structure should reflect the architecture of the solution, while dependency rules help preserve it as the project evolves.</p>
<h2>Experience Using NX</h2>
<p>My first substantial experience with NX came from a large Angular project that started in 2017 and is still running today. By that point, the workspace contained two applications and around ten libraries. As it grew, understanding and controlling the dependencies between them became increasingly difficult. Circular dependencies were especially troublesome: finding them and sorting them out manually was not easy.</p>
<p>One of the first steps was to split the project configuration. Instead of one large <code>angular.json</code>, each project had its own <code>project.json</code>. This describes the experience with the earlier project, not the initialization of the new workspace covered in this article.</p>
<p>The main benefit, though, came from Module Boundaries rules. They made it possible to define which libraries were allowed to depend on one another, using tags to identify their groups and functional areas.</p>
<p>These rules became more than agreements between developers: ESLint checked them during code analysis, and violating configured constraints could fail the build. This made it easier to control dependencies, prevent circular relationships, and maintain the architecture as the project evolved.</p>
<h2>Initializing an NX Workspace</h2>
<p>Before creating the first Angular application, let's prepare the repository.</p>
<p>The <code>enterprise-frontend-workshop</code> repository already contains articles, documentation, and architectural principles. Rather than creating a separate repository for the practical example, we decided to add NX directly to the existing one. This lets us evolve the application alongside its documentation and keep the history in a single Git repository.</p>
<p>To initialize NX, run:</p>
<pre><code class="language-bash">npx nx@latest init
</code></pre>
<p>This starts an interactive NX setup process. The installer presents several options, and we need to make a decision at each step.</p>
<h3>Choosing the Installation Method</h3>
<p>First, NX asks how it should be added to the project. We choose <code>package.json installation</code>, which installs NX through the existing npm project.</p>
<p>NX is added to the current repository rather than creating a new project in a separate directory.</p>
<h3>Choosing the Setup Mode</h3>
<p>Next, the installer offers a minimal setup or a guided one. We choose <code>Guided</code>.</p>
<p>This mode walks us through the available settings so we can make decisions step by step.</p>
<h3>Configuring Additional Features</h3>
<p>The installer then asks about AI agents. We choose <code>none</code>, because integrating AI tools is outside the scope of this stage.</p>
<p>Next comes NX Cloud, which provides remote caching and distributed task execution. We choose <code>Skip for now</code>. These capabilities are not required to get the workspace started, and we can revisit them when there is a concrete need.</p>
<p>Finally, the installer asks whether we agree to share usage data with the NX team. We choose <code>No</code>, and the installer confirms that analytics are disabled.</p>
<h3>Initialization Results</h3>
<p>The installer finishes with <code>Done!</code> and provides documentation about adding NX to an existing project.</p>
<p>The following configuration files appear in the repository root:</p>
<ul>
<li><code>nx.json</code> — the initial global NX configuration. At this point, it contains a JSON Schema reference and the setting that disables analytics.</li>
<li><code>package.json</code> — the npm project manifest and the <code>nx</code> dependency at version <code>24.0.1</code>.</li>
<li><code>package-lock.json</code> — the locked npm dependency tree.</li>
<li><code>.gitignore</code> — rules for excluding local IDE settings, NX cache data, and the <code>node_modules</code> directory.</li>
</ul>
<p>The <code>.nx/</code> and <code>node_modules/</code> directories also appear. The first stores local NX data; the second contains installed npm packages.</p>
<p>At this point, we have a minimal NX configuration. The repository does not yet contain an Angular application or our own libraries.</p>
<p>Adding NX prepares the infrastructure; it does not automatically create the application's architecture. The project structure and dependency rules will take shape as the solution evolves.</p>
<p>The changes are kept in a separate Git branch, <code>NX-Workspace</code>, on GitHub. This allows the practical implementation to evolve separately from the repository's main branch.</p>
<h2>Creating the Angular Application</h2>
<p>With the NX Workspace initialized, the next step is to create the first application.</p>
<p>The goal is not just to generate an Angular app. We will evolve Personal Finance incrementally and observe how its architecture changes as new requirements emerge. For now, we need a minimal but complete foundation: an application and the configuration required for development, testing, and code-quality checks.</p>
<h3>Why Angular?</h3>
<p>Angular is the framework chosen for this example. It is the main frontend framework in my work, and it provides a useful basis for demonstrating how an enterprise application can be organized and evolved.</p>
<p>We are not going to create a complex architecture with numerous libraries and abstractions upfront. At this stage, there is no business functionality that would justify those decisions. First, we will establish a working foundation, verify the tooling, and introduce architectural elements when they become necessary.</p>
<h3>Generating the Application</h3>
<p>We use the NX Angular generator to create the application. First, inspect the available options:</p>
<pre><code class="language-bash">npx nx g @nx/angular:application --help
</code></pre>
<p>The generator supports configuration of the bundler, routing, testing, linting, styles, and other options. This lets us establish the project's basic conventions before development begins.</p>
<p>We choose the following settings:</p>
<ul>
<li>Application name: <code>personal-finance</code>.</li>
<li>NX plugin: <code>@nx/angular</code>.</li>
<li>Bundler: <code>esbuild</code>.</li>
<li>Routing: <code>enabled</code>.</li>
<li>Standalone components: <code>enabled</code>.</li>
<li>Unit tests: <code>vitest-angular</code>.</li>
<li>End-to-end tests: <code>Playwright</code>.</li>
<li>Static analysis: <code>ESLint</code>.</li>
<li>Styles: <code>SCSS</code>.</li>
<li>SSR: <code>disabled</code>.</li>
<li>Strict mode: <code>enabled</code>.</li>
<li>Zoneless: <code>enabled</code>.</li>
</ul>
<p>These choices reflect the goals of the workshop. We need a client-side application that can gradually gain routes and functionality. SSR is not required at this stage. Strict mode helps detect problems early, and SCSS is the chosen stylesheet format.</p>
<p>Zoneless is Angular's mode of change detection without a dependency on Zone.js. We enable it when generating the application rather than introducing the configuration later as a separate migration.</p>
<p>For unit tests, we choose <code>vitest-angular</code>. I have not used this tool in my previous work, and a new project gives us an opportunity to try a modern testing stack without having to preserve compatibility with existing infrastructure. This does not mean Jest is inferior or should not be used elsewhere. Vitest is simply the choice for this example, and we will verify it in practice.</p>
<p>For end-to-end testing, we choose Playwright. It can run browser-based tests across different browser engines. ESLint handles static code analysis, while TypeScript checks types. These tools complement one another rather than replacing one another.</p>
<p>Run the generator with the selected options:</p>
<pre><code class="language-bash">npx nx g @nx/angular:application \
  --name=personal-finance \
  --directory=apps/personal-finance \
  --bundler=esbuild \
  --routing=true \
  --standalone=true \
  --e2eTestRunner=playwright \
  --unitTestRunner=vitest-angular \
  --linter=eslint \
  --style=scss \
  --ssr=false \
  --strict=true \
  --zoneless=true
</code></pre>
<p>NX creates the application and a separate project for end-to-end tests.</p>
<h3>What Appears in the Workspace?</h3>
<p>The main parts of the new project are located in these directories:</p>
<pre><code class="language-text">apps/
├── personal-finance/
└── personal-finance-e2e/
</code></pre>
<p><code>personal-finance</code> is the Angular application, containing its source code, routes, styles, and TypeScript configuration.</p>
<p><code>personal-finance-e2e</code> is a separate project for end-to-end tests. It contains the Playwright configuration and the <code>example.spec.ts</code> test file.</p>
<p>The generator also updates <code>package.json</code>, <code>package-lock.json</code>, and <code>nx.json</code> with the required dependencies, plugins, and task settings. ESLint configuration is stored in <code>eslint.config.mjs</code>. We keep the modern Flat Config generated by the tool.</p>
<p>We will shape the application's architecture gradually, as functionality appears and there is a real need to separate responsibilities between projects and libraries.</p>
<h3>Checking the Generated Projects</h3>
<p>First, confirm that NX recognizes both projects:</p>
<pre><code class="language-bash">npx nx show projects
</code></pre>
<p>The output lists:</p>
<pre><code class="language-text">personal-finance-e2e
personal-finance
</code></pre>
<p>Next, check the build, unit tests, and static analysis:</p>
<pre><code class="language-bash">npx nx build personal-finance
npx nx test personal-finance
npx nx lint personal-finance
</code></pre>
<p>All three commands complete successfully. The production build is written to <code>dist/apps/personal-finance</code>, the unit test passes, and ESLint reports no violations of the configured rules.</p>
<p>Angular does, however, warn that the styles in the generated <code>nx-welcome.ts</code> component are 7.03 KB, exceeding the configured 4 KB budget. We do not increase the budget just to accommodate a template component. After replacing the demo interface with our own, we can check whether the warning remains.</p>
<h3>Checking End-to-End Tests</h3>
<p>Run the Playwright tests:</p>
<pre><code class="language-bash">npx nx e2e personal-finance-e2e
</code></pre>
<p>The first run fails because Playwright cannot find the browser executables for Chromium, Firefox, and WebKit. The npm package is installed, but the browsers themselves still need to be downloaded.</p>
<p>Install them with:</p>
<pre><code class="language-bash">npx playwright install
</code></pre>
<p>After installation, the tests pass in all three browsers.</p>
<p>The <code>example.spec.ts</code> test checks that the page heading contains the text <code>Welcome</code>:</p>
<pre><code class="language-typescript">import { test, expect } from '@playwright/test';

test('has title', async ({ page }) =&gt; {
  await page.goto('/');
  expect(await page.locator('h1').innerText()).toContain('Welcome');
});
</code></pre>
<p>This is a demonstration test. It verifies basic browser interaction with the application, not business functionality.</p>
<h3>Results of This Stage</h3>
<p>We now have a working NX Workspace with an Angular application and a separate end-to-end test project. The build, unit tests, ESLint, and Playwright all work.</p>
<p>Two details came up during setup: the generated demo component exceeds the configured style budget, and Playwright requires a separate browser installation. We have postponed the first issue until the demo interface is replaced and resolved the second by installing the browsers.</p>
<p>We started with a minimal application, checked the basic infrastructure, and confirmed that the tools work. We can now move on to developing Personal Finance and make architectural decisions as real requirements emerge.</p>
<h2>Implementation Branches</h2>
<ul>
<li><a href="https://github.com/iAVP/enterprise-frontend-workshop/tree/NX-Workspace">NX-Workspace</a> — NX Workspace initialization.</li>
<li><a href="https://github.com/iAVP/enterprise-frontend-workshop/tree/NX-Workspace+1">NX-Workspace+1</a> — adding Angular and creating the first application.</li>
</ul>
<h2>Further Reading</h2>
<p>The following official resources provide more details about the tools and concepts covered in this article:</p>
<ul>
<li><a href="https://nx.dev/docs">Nx Documentation</a> — the main documentation portal.</li>
<li><a href="https://nx.dev/docs/reference/workspace">Nx Workspace</a> — workspace concepts and setup.</li>
<li><a href="https://nx.dev/docs/reference/project-configuration">Nx Project Configuration</a> — how Nx projects and their targets are configured.</li>
<li><a href="https://nx.dev/docs/technologies/angular/generators">Angular Application Generator</a> — available options for generating Angular applications.</li>
<li><a href="https://nx.dev/docs/features/enforce-module-boundaries">Enforce Module Boundaries</a> — controlling dependencies between projects using tags and lint rules.</li>
<li><a href="https://nx.dev/docs/technologies/angular/introduction">Nx with Angular</a> — integrating Angular applications with Nx.</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The Road to Architecture]]></title><description><![CDATA[How architectural thinking develops through experience, technical decisions, maintainability, project economics, and building software that evolves.]]></description><link>https://enterprise-frontend-workshop.hashnode.dev/the-road-to-architecture</link><guid isPermaLink="true">https://enterprise-frontend-workshop.hashnode.dev/the-road-to-architecture</guid><category><![CDATA[software architecture]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[frontend]]></category><category><![CDATA[clean code]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Andrey Pankov]]></dc:creator><pubDate>Mon, 26 Oct 2026 11:29:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb625d2c8778db47f3c09f/a6bb9012-061c-4904-adb4-e9f0761191ff.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I started with C++. Then came Delphi. After that, there were other tools, platforms, and technologies. Looking back, almost everything has changed over the years: programming languages, operating systems, approaches to interface development, tools, and even our understanding of what software should look like.</p>
<p>Yet some questions have remained the same.</p>
<p>I kept encountering situations where a solution that worked perfectly well under certain conditions began to cause problems under others. The differences between 16-bit, 32-bit, and 64-bit systems, the peculiarities of rendering and display, interface development, and platform limitations — all of this required more than simply knowing a programming language. I needed to understand how the system worked, why it behaved the way it did, and what consequences a particular implementation choice would have.</p>
<p>Back then, suitable tools or ready-made solutions were not always easy to find. Sometimes, I had to look for workarounds, explore third-party libraries, and learn to use products that offered capabilities missing from Microsoft's standard development tools.</p>
<p>Over time, I made my way from Borland products to JetBrains tools, Android development, and modern frontend technologies.</p>
<p>It wasn't simply a transition from one development environment to another. Every new platform brought its own limitations, possibilities, and questions that needed answers.</p>
<p>Gradually, I found myself thinking more and more not just about how to implement a particular feature, but also about which solution would make sense in the long run. How difficult would it be to maintain? What would happen when the requirements changed? How much time would further development take? Could today's convenient solution become a source of constant problems tomorrow?</p>
<p>And, of course, there was always the question of cost. Development time is limited, project resources are finite, and the client needs a result that makes sense not only technically but economically as well.</p>
<p>Money is not just the lifeblood of war. It is also the lifeblood of any complex technical process — and especially of the IT industry. It makes it possible to develop, maintain, and evolve software systems. And sometimes, economic circumstances rather than an architect's technical preferences determine which technology or architectural approach will ultimately be chosen.</p>
<p>It is easy to get carried away by a new, fashionable framework, enthusiastically start designing a system around it, and only later discover that the price of that decision is too high. Sometimes, it is far more useful to pause, carefully examine the available options, and consider what will happen to the project several years down the road.</p>
<p>How will it evolve? How much will it cost to maintain? How easy will it be to find people who can work with it? Could today's technological advantage become tomorrow's limitation?</p>
<p>For me, architecture is also about resisting the first impulse, however attractive it might seem. It means not reaching for the latest tool simply because it has appeared and promises to solve every problem, but first understanding what it will actually bring to the project — and at what cost.</p>
<p>Sometimes, the right choice is a new technology. Sometimes, it is a tool that has stood the test of time. And sometimes, the more sensible decision is to abandon the idea of building something ourselves and use an existing product instead.</p>
<p>What matters is that the choice should be driven neither by fashion nor by a developer's personal preferences, but by an understanding of the problem, the project's long-term prospects, and the resources available to us.</p>
<p>Over time, this perspective began to take shape not only through working with technologies, but also through working with existing projects. Starting development from scratch and gradually making decisions as new requirements emerge is one thing. Arriving at a project where much of the work has already been done, deadlines are approaching, and the team is struggling to meet requirements that somehow demand more and more time and effort is quite another.</p>
<p>In such situations, the consequences of decisions made long before your arrival become particularly visible. They are not always documented, explained in code comments, or even consciously recognized by anyone. Yet they are there — in the application's structure, in the way its parts interact, in duplicated code, and in the habits developers have formed when solving problems.</p>
<p>Sometimes, simply looking at how a project is organized is enough to understand why its development is progressing more slowly than expected. New functionality becomes so tightly coupled with existing code that every change requires checking numerous other parts of the system. The same code has to be fixed in several places. Developers spend their time not so much solving the actual problem as figuring out where something needs to change and what might stop working as a result.</p>
<p>And yet, the problem is not always a lack of experience on the part of individual programmers. Each of them may be perfectly capable of handling their immediate tasks, while the team's overall results still fall far short of expectations. Sometimes, the cause lies at another level: in how the project is organized, how clearly its boundaries are defined, and whether there are shared rules that allow different people to work on the same system effectively.</p>
<p>I gradually began to notice that these problems kept recurring regardless of the technologies being used. Programming languages, libraries, build tools, and approaches to interface development changed, but the consequences of certain decisions remained remarkably similar. Code became harder to modify, responsibilities between application components became less clear, and a seemingly simple task gradually turned into a chain of changes that had to be coordinated with one another.</p>
<p>This was where the difference between writing working code and building a system that could continue to evolve became increasingly apparent to me.</p>
<p>Working code solves today's problem. But a software product rarely remains unchanged after its first release. Requirements evolve, new use cases emerge, functionality expands, and new developers join the team. Sometimes, the platform or environment in which the application must run changes as well. That is when it becomes clear how well the original decisions accounted for future changes.</p>
<p>Of course, it is impossible to anticipate everything that might happen to a product over several years. Trying to do so would be pointless: requirements can change so significantly that some of the original assumptions no longer make sense. But there is a substantial difference between trying to predict the future in every detail and not thinking about it at all.</p>
<p>I became increasingly concerned with understanding which decisions we were making and how expensive they might be to change later. Where could we afford a simple solution that would be easy to replace if necessary? And where was it worth spending additional time on discussion and design because a mistake would affect too much of the system?</p>
<p>Over time, I came to believe that architecture is not about defining every class, component, or method in advance. Its purpose is much broader. It helps establish the system's fundamental boundaries, distribute responsibilities, define how its parts interact, and create conditions in which further development does not continually make the existing system more complicated.</p>
<p>And the more experience I gained, the less I wanted to think of architecture as merely a technical construction.</p>
<p>After all, even a well-designed system does not exist in isolation. It is built by real people with particular experience, knowledge, and habits. They work within deadlines, budgets, and technical constraints that are not always under their control. A solution that looks convincing on an architectural diagram may turn out to be entirely impractical if the team does not understand its principles or lacks the skills needed to implement and maintain it.</p>
<p>I had to learn to account for that, too.</p>
<p>I began to approach project preparation differently. It was no longer enough simply to gather requirements, select a suitable set of technologies, and determine which tasks needed to be implemented. I wanted to understand first who would be working on the system, what experience the future team members had, what constraints the existing environment imposed, and what the product was actually expected to become.</p>
<p>Discussing the MVP, future plans, the team's capabilities, and the client's requirements gradually became part of my architectural work. Not a separate organizational stage to complete before programming began, but a way of understanding the problem itself.</p>
<p>An architectural decision cannot be separated from the conditions under which it must be implemented. What works well for one team and one product may be a poor choice for another. Even a familiar technology that developers have used for years is not always the optimal option. Conversely, there is no need to introduce a new tool simply because it offers additional capabilities.</p>
<p>I believe it is important to understand what we want to achieve and what conditions we need to account for before choosing a particular implementation.</p>
<p>At the same time, I do not believe that every decision must be finalized before development begins. On the contrary, some things only become clear as the work progresses. Sometimes, an assumption needs to be tested in practice, feedback needs to be collected from users, or an approach needs to be evaluated under real-world conditions. In such cases, changing an initial decision is not a mistake. It is a natural part of development.</p>
<p>But some decisions have consequences that extend across the entire project. They determine how the system's parts interact, how freely individual components can evolve, how the team distributes responsibilities, and how much effort further development will require. If these decisions are neglected at the beginning, it may later turn out that changing one part of the application without substantially restructuring the others is almost impossible.</p>
<p>That is why I have always wanted to distinguish between the things that genuinely need to be thought through in advance and those that can be refined as the project evolves.</p>
<p>Perhaps this is one of the most significant changes in my approach to development. Earlier in my career, most of my attention naturally went to implementation itself: how to solve a problem, make the program work, and use the capabilities of the chosen technology. With experience, the question of how a decision would affect the product's future became increasingly important.</p>
<p>Not because technical implementation had become less important. It was simply no longer the only part of the work that determined the outcome.</p>
<p>Ultimately, software is not created merely to pass a test once or demonstrate the capabilities of a particular framework. It must solve users' problems, evolve alongside business needs, and remain understandable to the people responsible for maintaining it. If architecture helps achieve all of this, then it is doing its job.</p>
<p>Today, when starting a new project, I try not to rush into choosing technologies. First, I want to understand the problem, talk to the people who will be working on the product, identify the constraints, and discuss how we see its future development. Only then do I move on to specific architectural decisions.</p>
<p>This does not mean that I have a universal blueprint suitable for every project. Rather, over the years, I have become accustomed to asking myself and the team questions that help us avoid overlooking important circumstances or making decisions purely out of habit.</p>
<p>Why are we choosing this approach? What problem does it solve? What alternatives are available? What happens if the requirements change? How difficult will the system be to maintain several years from now? And are we prepared to accept the limitations that inevitably come with our chosen solution?</p>
<p>The answers are not always obvious, and sometimes these discussions take longer than we would like. But I believe that spending time on such questions at the beginning of a project is far more sensible than discovering a year or two later that the system's foundation no longer allows us to move forward without substantial changes.</p>
<p>Perhaps that is why, over the years, I have become more cautious about bold technological promises and universal architectural recipes. I have seen solutions that seemed convenient and justified at the time later create additional difficulties. I have also seen the opposite: how changing an approach at the right moment could significantly simplify further work and free up resources for product development.</p>
<p>I do not want to turn this experience into a set of rules that everyone should follow. Every project has its own circumstances, constraints, and goals. What proved useful in one situation will not necessarily be the right choice in another.</p>
<p>But I am convinced that understanding why we make architectural decisions is no less important than knowing the technical tools themselves. You can know a particular framework inside out, understand design patterns, and be proficient with modern technologies, yet still choose the wrong solution if you fail to understand the problem and its context.</p>
<p>That is why I want to write not only about how particular solutions work, but also about why I arrived at them, what problems I was trying to solve, and which circumstances influenced my choices.</p>
<p>In future articles, I plan to revisit individual projects and examine some of these decisions in greater detail. There will be more technical detail, concrete examples, and explanations of how application structures changed, why the organization of code had to be reconsidered, and how these changes affected subsequent development. Perhaps, in some cases, a different approach would have worked just as well. That, too, is part of engineering: considering alternatives, understanding their limitations, and making decisions based on the circumstances at hand.</p>
<p>For me, this article is an attempt to explain where this way of thinking about architecture came from in the first place.</p>
<p>I did not arrive at it in a single day, and I cannot point to one particular moment when everything suddenly became clear. It happened gradually. Different projects, different teams, different technologies — and the recurring need to understand the consequences of decisions that had once seemed perfectly reasonable.</p>
<p>Over time, these observations came together into a broader understanding: architecture does not begin with diagrams, the choice of a framework, or even the structure of directories in a repository. It begins with an attempt to understand what kind of system we are building, whom it is intended for, who will develop and maintain it, and which decisions genuinely need to be made before a substantial amount of code has been written.</p>
<p>For me, architecture has become a way of thinking about a product as a whole rather than focusing only on its individual features. A way of accounting not only for today's results, but also for the possibility of future development. And perhaps, above all, a way of making deliberate decisions while understanding that every decision has a cost — and that this cost is not always paid immediately.</p>
<p>Sometimes, the consequences only become visible years later. But that is precisely why they are worth considering in advance.</p>
<p>Not to eliminate every mistake or anticipate every possible situation, but to leave the system room to change, give the team the opportunity to work without constantly fighting its own code, and allow the product to continue evolving.</p>
<p>Perhaps that is where architecture begins for me.</p>
<p>Not with the search for a perfect solution, but with understanding why we choose a particular path and where that choice might lead us.</p>
<hr />
<p>#SoftwareArchitecture #SoftwareEngineering #EnterpriseArchitecture #FrontendArchitecture #TechnicalLeadership #CleanCode #EnterpriseFrontendWorkshop</p>
]]></content:encoded></item></channel></rss>