كل المقالات والفيديوهات

تصميم أنظمة الواجهات الأمامية لتطبيقات المؤسسات: 8 قرارات

تصميم أنظمة الواجهات الأمامية لتطبيقات المؤسسات حُكمٌ لا قاموس مصطلحات: ثمانية مجالات، والمفاهيم القليلة التي تهمّ في كل منها، وماذا تختار ومتى.

Adil Ben Moussa
08 نوفمبر 2026· 4 دقائق
تصميم أنظمة الواجهات الأمامية لتطبيقات المؤسسات: 8 قرارات

تعيش الواجهة الأمامية في المؤسسات أطول من إطارها البرمجي، ومن فريقها الأول، وغالباً من مهندسها المعماري الأول. وهذا يغيّر المفاهيم التي تهمّ. لم تكن المفاهيم يوماً الجزء الصعب، بل معرفة ما يمكن تجاهله منها.

معظم ما يُكتب عن تصميم أنظمة الواجهات الأمامية يُعدّك لمقابلة عمل: صمّم صفحة أخبار متجددة في 45 دقيقة، على سبورة، لمستخدم لا وجود له. أما الواجهات الأمامية في المؤسسات فتفشل بطريقة أخرى. لا تفشل في اليوم الأول، بل في السنة السابعة، حين يصبح القرار الذي لم يتخذه أحد عن قصد حاضراً في أربعمئة ملف.

أمضيت أكثر من عشرين عاماً في البرمجيات، أبني تطبيقات للمؤسسات. وهذه السلسلة خلاصة ما أنصح به فريقاً يبدأ تطبيقاً من هذا النوع، أو يرثه.

ما الذي يجعل الواجهة الأمامية «مؤسسية»

ليس حجم الحزمة (bundle)، بل خمسة أمور قلّما تجتمع في التطبيقات الصغيرة.

الزمن. يعيش الكود أطول من الإطار. انتهى دعم في يناير 2022. لكن التطبيقات المبنية عليه لم تنتهِ معه. شاركت في عدة عمليات ترحيل () من AngularJS إلى ، في فرق وأدوار مختلفة. والدرس في كل مرة واحد: الحدود التي ترسمها في السنة الأولى تقرّر ما تستطيع حذفه في السنة العاشرة.

الفرق. فرق كثيرة في قاعدة كود واحدة، وغالباً في (monorepo) واحد. والخطوط الفاصلة بين كود كل فريق وآخر هي المعمارية، سواء رسمها أحد أم لم يرسمها.

القواعد. يأتي القطاع البنكي وقطاع الصحة بقواعد لا تستطيع الواجهة الأمامية تجاوزها. فالتحكم في الوصول () «لا يكون فعّالاً إلا إذا نُفّذ في كود موثوق على الخادم أو في واجهات برمجية دون خادم»، وما زال خلل التحكم في الوصول يتصدّر قائمة :2025. وفي الاتحاد الأوروبي، يشمل القانون الأوروبي لإمكانية الوصول () الخدمات البنكية المقدَّمة للمستهلكين بعد 28 يونيو 2025. وبالنسبة إلى البنوك المعنية، تُعدّ إمكانية الوصول هناك شرطاً، لا ميزة إضافية.

الأدوار. الشاشة نفسها تبدو مختلفة للعميل، وللمستشار، وللمدقّق. وعلى أحدٍ ما أن يقرّر أين يعيش هذا المنطق.

اللغات. في المغرب والخليج، تُكتب العربية من اليمين إلى اليسار، والنص الفرنسي غالباً أطول من الإنجليزي، والعملة المغربية هي الدرهم (MAD) بتنسيقه الخاص. والتخطيط المبني على left و right ينكسر يوم تصل العربية. تصف في ‏(logical properties) الاتجاهات «نسبةً إلى اتجاه كتابة المحتوى، لا إلى اتجاه مادي»، وهذا الاختيار وحده يقرّر هل يكون دعم الكتابة من اليمين إلى اليسار () مهمة صغيرة أم إعادة كتابة.

في المؤسسات، يعيش القرار أطول ممن اتخذوه. صمّم للفريق القادم.

الأجزاء الثمانية

ثمانية مجالات، وفي كل منها فقط المفاهيم الخمسة إلى السبعة التي تغيّر قراراً.

  1. المعمارية. الطبقات، والحدود التي تُفرض آلياً، والمستودع الموحّد أو عدمه، والواجهات المصغّرة () في الغالب لا، والركن القديم الذي تُبقيه قابلاً للحذف.
  2. الحالة. من يملك كل جزء من الحالة (state)، وكم يعيش. بيانات الخادم، والواجهة، والنماذج، والرابط.
  3. الشبكة والتخزين المؤقت. سؤال واحد، لذلك جزء واحد: أين تعيش هذه البيانات، ومتى تصبح خاطئة؟
  4. العرض. كثير من تطبيقات المؤسسات التي تتطلب تسجيل الدخول لا تحتاج إلى العرض على الخادم. متى تحتاج إليه، وما كلفته.
  5. الأداء. ميزانيات تُفشل البناء، وتُقاس على الأجهزة التي يملكها مستخدموك فعلاً.
  6. الأمان. الواجهة الأمامية هي الجزء غير الموثوق. ما يستطيع العميل حمايته، وما يكتفي بعرضه.
  7. إمكانية الوصول. شرط قانوني، ونظام التصميم هو المكان الذي تُكسب فيه أو تُخسر.
  8. اللغات، والكتابة من اليمين إلى اليسار، والعملة. العربية والفرنسية والدرهم من أول مكوّن، لا مشروعاً لاحقاً.

يأتي التخزين المؤقت مع الشبكة لأن الذاكرة المؤقتة (cache) التي لا مالك لها خللٌ مؤجَّل. ويحظى دعم الكتابة من اليمين إلى اليسار بجزء خاص به لأنه، عند القرّاء في المغرب والخليج، ليس بنداً فرعياً تحت إمكانية الوصول.

ابدأ بـ الجزء الأول، المعمارية.

كيف يُبنى كل جزء

لكل جزء البنية نفسها:

  • المفاهيم القليلة التي تهمّ، ولكل منها سطر واحد يشرح السبب.
  • ماذا تختار، ومتى. (Signals) أو ، والعرض على جهة العميل () أو على الخادم ()، والمستودع الموحّد أو المستودعات المتعددة (). الموازنة بكلمات بسيطة، Angular أولاً، و إلى جانبه حيث يختلف الاختيار.
  • ما تركته جانباً، ولماذا. مفهوم يبدو مهماً، لكنه نادراً ما يغيّر قراراً في تطبيق مؤسسي.
  • مثال في Atlas Bank، التطبيق التجريبي المفتوح لسلسلة الذكاء الاصطناعي مع Angular: بنك مغربي خيالي، مبني كمستودع موحّد على ، بحدود بين الوحدات يفرضها npm run lint:arch، وفيه ركنان قديمان (شاشة تحويلات بـ NgModule وشاشة مستفيدين بـ AngularJS). المستودع عام. ولا كود عملاء أبداً.
يخبرك قاموس المصطلحات بمعنى الكلمة. أما الإنتاج فيخبرك بالكلمات التي يمكنك تجاهلها.

مع بداية الأسبوع

  • خذ أقدم شاشة في تطبيقك. واسأل عن كل مجال من المجالات الثمانية: هل اتُّخذ هذا القرار عن قصد، أم فرضه الوضع الافتراضي؟
  • دوّن المجال الذي لا تجد له جواباً. فهذا أول سجل لقرار معماري () تكتبه.
  • ابدأ بـ صفحة المعمارية في Atlas Bank: تعرض عقد الاعتماديات الذي يقوم عليه الجزء الأول.

لوكيل البرمجة الخاص بك

قواعد الأجزاء الثمانية في ملف واحد، مكتوبة لوكيل. الصقها في ملف AGENTS.md لديك، أو احفظها مهارةً ()، أو وجّه وكيلك إلى الملف. ولكل جزء أيضاً ملفه الخاص في نهاية المقال.

لوكيل البرمجة الخاص بك تنزيل ملف ‎.md
# Frontend system design rules for enterprise apps

Source: https://tt-tech.io/series/frontend-system-design
For Angular and React frontends. All eight parts of the series in one file. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ.

## Frontend architecture rules for enterprise apps

Source: https://tt-tech.io/blog/frontend-architecture-enterprise-boundaries

### Rules

1. Split code by type, not only by feature: `domain` (pure models and rules), `data-access` (the only code that talks to the backend), `ui` (inputs and outputs only), `feature` (wires the three) and `app` (shell and routes). Put new code in the layer it belongs to.
2. Keep the "may import" contract: `ui` never imports `data-access`, `domain` imports nothing, and nothing modern imports the legacy code.
3. Enforce boundaries in lint, not in a wiki. On Nx, tag every project and set which tags may depend on which (`onlyDependOnLibsWithTags`). Without Nx, use Sheriff.
4. When the boundary lint fails, let it fail: fix the import, or record the crossing. Never switch the rule off to make the change pass.
5. When a boundary must move, write it down in an Architectural Decision Record (ADR) with the rationale, and reference it in the pull request.
6. Treat the design system as a library under the same rules. Components read colour from tokens by role, never from a raw hex value.
7. Keep the legacy corner frozen and wrapped. No new code imports it. Dependencies point one way, from old to new, so the old code stays deletable.
8. Treat monorepo or polyrepo as an organisational choice. One repository with boundaries inside fits teams that share a design system, a domain model and one release train. Separate repositories fit products that ship on their own schedules and share almost nothing, or when strict repository-level access control is a hard requirement.
9. Do not introduce micro-frontends to fix a messy codebase. They fit when separate organisations must deploy independently into one shell. Fix the missing boundary first.

### Before you say done

- Run the project's boundary lint (Nx module boundaries or Sheriff) and fix every new error.
- Search the diff for imports from `ui` into `data-access`, from modern code into legacy code, and for raw colour values in stylesheets.
- List every boundary crossing in the pull request with its ADR.

## State rules for enterprise apps

Source: https://tt-tech.io/blog/enterprise-frontend-state-ownership

### Rules

1. Before you add state, name its kind: server data, UI state, form state, URL state or session state. Store it as that kind.
2. Put anything a link, a bookmark, a support ticket or the back button must restore in the URL: filters, tabs, the selected record. In Angular, bind route and query parameters to inputs with `withComponentInputBinding`, and expect a missing parameter to arrive as `undefined`.
3. Server data belongs to the backend. Never copy it into a component. Read it from the store of its domain.
4. Give each domain its own store (accounts, cards, requests), owned by the team that owns the domain. Never add a single global store for everything.
5. Keep global only what is cross-cutting by nature: the session, the permissions, maybe feature flags. Do not cache roles in a store nobody refreshes.
6. Keep UI state (which panel is open) in the component that owns it.
7. Derive, do not duplicate. If a value can be calculated from state, use `computed` in Angular or calculate it during render in React. Never store it as a second copy.
8. Never use `effect` to propagate state changes. Use `computed` instead.
9. Keep a store's writable state private. Expose read-only signals with `asReadonly()` and derived values, so the store is the only owner.
10. In long regulated forms, keep drafts, mirror the server's validation and track dirty state. The server still validates; the client check is for the person typing.
11. Use Signal Forms in new code built with signals. Keep reactive forms in an existing app that uses them. Do not migrate forms for fashion.
12. Use signals for state (a value now) and RxJS for events over time (keystrokes, websockets, retries). Bridge them with `toSignal`.

### Before you say done

- For every new piece of state in the diff, list its kind and its owner in the pull request.
- Search the diff for `effect(` calls that set other state, and for stored values that could be a `computed` or a calculation during render.
- For every new filter, tab or selection, confirm it lives in the URL, or say why not.

## Networking and caching rules for enterprise apps

Source: https://tt-tech.io/blog/enterprise-frontend-networking-caching

### Rules

1. Components never fetch. All backend calls go through one data-access layer per domain. No `fetch(` or `HttpClient` outside it.
2. Add a backend for frontend (BFF), owned by the frontend team, only when the screens need a shape the domain APIs do not have, or to keep secrets out of the browser.
3. Generate API types from the OpenAPI document. Never write them by hand. Treat a field as always present only when the schema's `required` array names it.
4. Cancel requests the user left: `httpResource` in Angular, `AbortController` with `fetch`. An older response must never overwrite a newer one.
5. Dedupe requests already in flight, so several widgets asking for the same data cause one request.
6. Give every call a timeout, so a slow backend becomes an error message, not an endless spinner.
7. The code that performs a write owns the cache of that data. After the write, refetch or apply the server's answer in the owning store. Never keep a private copy in a widget.
8. Set `Cache-Control` on purpose: `no-store` for responses that must not be reused (an account balance), `private` for user-personalised content, never personalised content in a shared cache, and a long `max-age` plus `immutable` for hashed build assets.
9. Retry reads only. Never retry a `POST` or `PATCH` automatically unless it carries an idempotency key that the server deduplicates.
10. On `409 Conflict`, show the conflict to the user and reload. Do not retry.
11. Put cross-cutting concerns in interceptors: auth headers, timeouts, read retries, logging, and a correlation id per request so server logs can be matched to what the user clicked.
12. On mobile, where the connection is often slow or gone, cache first: show the cached copy at once and refresh it in the background (stale-while-revalidate), with a time to live per entry. The write still clears what it makes wrong.

### Before you say done

- Search the diff for `fetch(` and `HttpClient` outside the data-access layer. Move each hit behind the boundary.
- For every write in the diff, list in the pull request which cached data it makes wrong and where it is refreshed.
- Check every retry in the diff: it is on a read, or the write carries an idempotency key.
- For every new or changed sensitive response, state its `Cache-Control` header.

## Rendering rules for enterprise apps

Source: https://tt-tech.io/blog/enterprise-frontend-rendering-csr-ssr

### Rules

1. Decide rendering per page, not per app. The line is the login: pages behind it use client-side rendering (CSR) by default.
2. Do not add server-side rendering (SSR) to dashboards, back office or other pages behind a login by default. It adds a server to run, patch and audit.
3. For public pages, prerender what is the same for everyone and server-render what changes per request. In Angular, set it per route with `RenderMode.Client`, `RenderMode.Prerender` and `RenderMode.Server`.
4. Code that runs under SSR must not depend on browser APIs. Keep hydration on with `provideClientHydration()`, so Angular reuses the server DOM instead of re-rendering it.
5. Use incremental hydration (`@defer` with `hydrate` triggers such as idle, viewport or interaction) for a heavy widget on a public page. It is not a reason to add SSR.
6. Put heavy widgets below the fold (charts, rich-text editors, PDF viewers) in `@defer`, for example `@defer (on viewport)`.
7. Deferred dependencies must be standalone. Code that still lives in an `NgModule` cannot go in `@defer`; load it through a lazy route instead.
8. Split lazy routes along team lines: one feature library, one route, one chunk. Load primary landing pages eagerly, as Angular recommends, and avoid deeply nested lazy loading.
9. Never rely on Zone.js to notice a change. Trigger rendering through a signal read in the template, an input, a template listener, or `markForCheck()`.
10. Treat every React Server Function (`'use server'`) as a public endpoint. Its arguments are client-controlled: validate them and check that the logged-in user may perform the action.

### Before you say done

- For every new or changed route, state whether it is behind the login and which render mode it uses.
- When you add or move a `@defer` block or a lazy route, run the production build and report the initial bundle size before and after.
- Search changed components for plain fields the template reads and code mutates. Make them signals, or name the notification that renders them.
- Search the diff for `'use server'`. For each function, name where it validates its arguments and checks the user.

## Frontend performance rules for enterprise apps

Source: https://tt-tech.io/blog/frontend-performance-enterprise-budgets

### Rules

1. Keep a `budgets` section in the build configuration (`angular.json` or `project.json`) with `maximumWarning` and `maximumError`. Start with `initial` and `anyComponentStyle`. Add per-bundle budgets when a lazy feature starts to grow.
2. When there is no budget, or the error limit is far above today's size, set it just above the current initial bundle.
3. Load each feature through its own lazy route. Code that belongs to another screen goes in a lazy route; a heavy widget below the fold on the same screen (a chart, an editor) goes in `@defer`. In React, use `lazy` with a `<Suspense>` fallback.
4. Track every `@for` by a stable id (`track item.id`), never by object reference. Give React lists rendered with `.map()` a stable key in the same way.
5. For long lists, paginate on the server when users search and filter. Use virtual scroll (`<cdk-virtual-scroll-viewport>`) when they really scan a long list on one page. Virtual scroll fixes rendering, not downloading.
6. In image-heavy Angular screens, use `NgOptimizedImage` (`<img ngSrc>`, not `<img src>`) with a width and height, and `sizes` for the `srcset`. Mark the main image of the page `priority`.
7. Judge Core Web Vitals at the 75th percentile of page loads, mobile and desktop apart, never by average. Good is LCP within 2.5 seconds, INP at 200 milliseconds or less, CLS at 0.1 or less.
8. On data-heavy screens (filters, sorting, opening a row), watch INP first.
9. Use lab data (Lighthouse in CI) to catch regressions and field data from real users to decide what to fix. A run on a developer machine is not what users get.
10. Add `preload`, `prefetch` or `preconnect` only when a measurement shows a resource arrives late, not by default.

### Before you say done

- Run the production build. It must pass the budgets. Report any new budget warning and the initial bundle size before and after.
- Search the diff for `@for` blocks that track by object or `$index`, and for `.map()` lists without a stable key.
- Search the diff for `<img src` in Angular templates, and for heavy libraries imported outside a lazy route or `@defer`.

## Frontend security rules for enterprise apps

Source: https://tt-tech.io/blog/frontend-security-enterprise-untrusted-client

### Rules

1. The browser is untrusted. Every access rule and business limit is enforced on the server. A check in a component is only for the user's screen.
2. When you add a client-side check (a limit, a role, a permission), add or find the same check on the server in the same change. If there is none, report it as a finding.
3. Take roles and permissions from the server's answer. Never decide what the user may do by decoding a token in the client.
4. Route guards are for navigation, not protection. Put `canActivateChild` on the parent route of an area, use `canMatch` for role-based variants of a path, and still expect the server to refuse.
5. Hide actions a role never has. Disable, with the reason shown, actions the user could have later (a limit, a missing signature).
6. Keep the sanitizer on. Never call `bypassSecurityTrust*` (Angular) or use `dangerouslySetInnerHTML` (React) on data from a user or an API. If some markup is wanted, map an allowed subset to elements in the template.
7. Never store session ids or tokens in `localStorage` or `sessionStorage`. Use an `httpOnly` cookie session through a backend for frontend, or a short-lived token in memory when the API is shared with other clients.
8. With cookie sessions, keep CSRF protection on: in Angular, `HttpClient` sends the `XSRF-TOKEN` cookie back as an `X-XSRF-TOKEN` header, and the server must check it.
9. No secrets in client code: no API key, token or password in the frontend, not even temporarily. Everything shipped to the browser is public.
10. The browser never calls a third-party service directly. The backend holds the key and makes the call.
11. Treat the Content Security Policy as a second layer: nonces when a server renders each response, `autoCsp` hashes when the app is static files. Do not loosen it to make a script run.
12. Never log tokens, session ids, passwords, personal data or bank and card data from the client. Mask them or leave them out. Send an interaction id with each request instead, so server logs can be matched.

### Before you say done

- Search the diff for `bypassSecurityTrust`, `dangerouslySetInnerHTML`, `localStorage` and strings that look like keys. Remove each hit or explain it in the pull request.
- For every client-side rule in the diff, name the server code that enforces it.
- List what you checked and what you could not check.

## Accessibility rules for enterprise apps

Source: https://tt-tech.io/blog/enterprise-frontend-accessibility-design-system

### Rules

1. Target WCAG 2.2 level AA.
2. Use native HTML elements first (`<label>`, `<dialog>`, native form controls). Use ARIA only for a widget HTML does not have.
3. When you add an ARIA role, implement in script the keyboard and behaviour the browser would give the native element. A role is a promise you keep in code.
4. Every modal dialog: focus moves inside on open, Tab stays inside (`cdkTrapFocus` in Angular), Escape closes, and on close focus returns to where it was before.
5. On a route change in a single-page app, move focus (in Angular, on `NavigationEnd`). Never let focus fall back to `body`.
6. In forms, give every field a real `<label>`, show field errors with `role="alert"`, and on long forms add an error summary that links to each field.
7. Announce something that happened elsewhere on the page ("Transfer sent") through a live region, `LiveAnnouncer` in Angular.
8. Submissions of financial data need a step where the user reviews and corrects before the submission is final (WCAG 3.3.4).
9. Fix accessibility in the shared design-system component, not in each screen. Focus rings, contrast and keyboard behaviour are decided there once.
10. For combobox, date picker and data grid, adopt a tested building block (Angular CDK, Angular Aria, or a mature React library chosen for its keyboard support). Build only what is specific to the domain.
11. Never add an accessibility overlay script as a fix. It does not fix the components underneath.
12. Run automated accessibility checks in CI, and never report a flow as accessible from automated checks alone. Login, payment and signature also need a manual keyboard and screen reader pass.

### Before you say done

- Run the project's accessibility lint (for example angular-eslint's template accessibility rules) and fix every new error.
- Search the diff for form error messages without `role="alert"` or a live region, and for `role=` attributes. For each role, name the code that implements its keyboard behaviour.
- For every new or changed dialog, list in the pull request where the code handles focus in, Tab, Escape and focus return.
- List the manual keyboard and screen reader checks still needed.

## Internationalisation and RTL rules for enterprise apps

Source: https://tt-tech.io/blog/arabic-french-mad-i18n-rtl-enterprise

### Rules

1. Choose build-time translation (Angular i18n, one build per locale) when the locale is fixed per user and the texts ship with the code. Choose a runtime library when users switch language often or texts come from a CMS.
2. Set `dir` explicitly on the root `html` element. Use the `dir` attribute instead of the CSS `direction` property where possible.
3. Use logical CSS properties: `margin-inline-start`, `padding-inline-end`, `border-inline-start`, `text-align: start`. Never add `margin-left`, `padding-right` or `text-align: left` in new code.
4. Mirror icons that show direction, such as back and forward arrows, in RTL. Do not mirror a play button or a logo.
5. Wrap text whose direction you do not control (names, user input, external data) in `<bdi>` or give it `dir="auto"`.
6. Mark values that are always left to right, such as a RIB or an IBAN, with `dir="ltr"`.
7. Format money through one function. Use `Intl.NumberFormat` for display across many locales; use a fixed, tested formatter when the format is a business decision.
8. Never assert an exact `Intl` output string in a test. It depends on the locale data the browser or Node version ships.
9. Build plurals and variable messages with ICU messages. Never concatenate translated strings.
10. Run design tokens, components and visual tests in RTL as well as LTR.
11. Use a font that renders Arabic well, not the Latin font's fallback. Leave room for translated text, which is often longer than English.
12. Let the user choose the language and remember the choice.

### Before you say done

- Search the diff for `margin-left`, `margin-right`, `padding-left`, `padding-right`, `text-align: left` and `text-align: right`. Replace each with a logical property or explain it.
- Search the diff for money formatting (`Intl.NumberFormat`, `toFixed`, currency codes) outside the shared formatter.
- Search the diff for string concatenation in translated text, and for names, user input or external values rendered without `<bdi>`, `dir="auto"` or `dir="ltr"`.

المصادر

  1. AngularJS, Version Support Status, التوثيق
  2. OWASP, A01 Broken Access Control - OWASP Top 10:2025, 2025
  3. EUR-Lex, Directive (EU) 2019/882 on the accessibility requirements for products and services, 2019
  4. , CSS logical properties and values
شارك هذا المقال