وب مدرن، رابط کاربری و تجربهی وب · فصل ۵ — Error Model، REST، Streaming و Webhook · 49
Fastify Architecture و Testable API
میتواند «Fastify Architecture و Testable API» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خلاصهٔ درس
میتواند «Fastify Architecture و Testable API» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خروجی این درس
میتواند «Fastify Architecture و Testable API» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
پیشنیازها
Happy Path میتواند سبز باشد و Architecture هنوز غلط باشد
فرض کنید سرویس Fastify از قبل infrastructure احراز هویت و database دارد. برای feature سفارشها یک route مستقیم روی root اضافه میکنیم، repository واقعی را داخل handler import میکنیم، body را بدون schema میپذیریم، در test یک port واقعی باز میکنیم و cleanup را به خروج process میسپاریم. Demo میتواند 201 برگرداند، اما این نتیجه درباره معماری چیز زیادی ثابت نمیکند. یک feature کناری ممکن است ناخواسته به orders repository وابسته شود، test ممکن است resource شبیه production را لمس کند، input خراب ممکن است وارد business code شود و resource باز با پایان process پنهان بماند.
WEB-005 Fastify Architecture را بهعنوان مسئله Evidence بررسی میکند. سؤال اصلی «این فایل در کدام folder باشد؟» نیست؛ سؤال این است که کدام Fastify Context مالک dependency است، چه descendantهایی آن را میبینند، Route Schema کدام مرز را محافظت میکند، dependency در test چگونه جایگزین میشود و کدام observation نشان میدهد lifecycle واقعاً بسته شده است. خروجی درس Fastify Boundary & Test Harness Record v1 است: رکوردی که Plugin Context، Dependency Boundary، Route Schema، In-Process Test، Lifecycle، یک Failure Mutation، repair و یک transfer مستقل را row-by-row ثبت میکند.
پنج family اصلی درس عبارتاند از Plugin Context، Dependency Boundary، Route Schema، In-Process Test و Lifecycle. این پنج مورد کنار هم هستند چون feature ایزولهای که dependency آن در test قابل تعویض نیست هنوز testable نیست؛ و test سبزی که فقط بهخاطر global بودن همه dependencyها میگذرد، boundary مفیدی را ثابت نمیکند.
محدوده و مسیر یادگیری
این درس distinctionهای Error Model در WEB-004 را prerequisite میگیرد و دوباره مالک public error taxonomy یا retryability نمیشود. WEB-006 مالک طراحی عمیقتر REST contract، idempotency و pagination است. WEB-005 روی سؤال کوچکتری تمرکز دارد: feature را در یک Fastify Encapsulation Context قرار بده، dependencyهایش را صریح کن، مرز request/response را با schema کنترل کن و API را بدون اینکه production listener تنها test seam باشد آزمایش کن.
اگر register، Decorator و Encapsulation برایت بیشتر syntax هستند تا model، مسیر foundation مناسب است. اگر از plugin استفاده میکنی ولی hidden import، global decorator یا test وابسته به infrastructure واقعی داری، مسیر repair را برو. اگر باتجربهای، syntax را فشرده کن و مستقیم context visibility، dependency replacement، lifecycle ownership و mutationها را بررسی کن. معیار نهایی همه مسیرها یکسان است.
این درس ادعا نمیکند Fastify Plugin همان Microservice یا Security Boundary است. ادعا نمیکند همه dependencyها باید روی root global باشند. ادعا نمیکند inject() رفتار reverse proxy، TLS یا socket واقعی را ثابت میکند. ادعا نمیکند JSON Schema برابر Business Correctness است. و ادعا نمیکند برای داشتن Testable Node.js API حتماً Fastify لازم است.
Mental Model: Context Graph مهمتر از Folder Tree است
مدل مفید Fastify یک graph از Contextهاست. Root Instance یک Feature Plugin را register میکند. طبق رفتار پیشفرض Fastify، register یک scope جدید ایجاد میکند. Decoration، Hook، Plugin و Route از قواعد Encapsulation Visibility پیروی میکنند: descendant میتواند چیزهای ancestor را به ارث ببرد، اما child decoration را به ancestor برنمیگرداند و sibling دیگر هم نباید آن را فرض کند. این جهت visibility از کنار هم بودن فایلها در repository مهمتر است.
Sequence اصلی را اینطور ببین:
- Root Context مالک composition و concernهایی است که واقعاً application-wide هستند.
- Feature Plugin boundary یک feature محدود مثل orders را میسازد.
- Decorator/Dependency Visibility مشخص میکند collaborator موردنیاز route دقیقاً در کدام context وجود دارد.
- Route Schema input و output transport را محدود میکند.
- Handler dependency را از boundary کنترلشده میگیرد، نه از hidden global state.
- Inject Test request را داخل process اجرا و response را observe میکند، بدون اینکه production port شرط اجباری باشد.
- Lifecycle Evidence نشان میدهد boot و close و resource ownership واقعاً قابل مشاهدهاند.
پس Architecture به معنی «همهچیز plugin باشد» نیست. یک dependency root-level وقتی چند feature مستقل عمداً آن را share میکنند میتواند درست باشد. یک dependency feature-local وقتی visibility باید همانجا متوقف شود مناسبتر است. تصمیم باید آنقدر صریح باشد که test بتواند dependency را replace کند و mutation نشان دهد چه کسی آن را میبیند.
Plugin Context: register و جهت Encapsulation
مستندات رسمی Fastify، register را مکانیزم اضافهکردن Plugin معرفی میکنند و میگویند بهصورت پیشفرض scope جدید ساخته میشود. نتیجه عملی، visibility جهتدار است. اگر Orders Feature در context خودش ordersRepo را Decorate کند، routeهای همان feature به آن دسترسی دارند. Root بهطور خودکار آن Decoration را دریافت نمیکند و sibling مستقل هم نباید وجود آن را فرض کند.
این رفتار در app کوچک بهراحتی اشتباه فهمیده میشود، چون شروع کار معمولاً با root instance است. اگر ordersRepo را روی root Decorate کنی، همه descendantها آن را میبینند و کار سریع جلو میرود. اما convenience دلیل ownership صحیح نیست. اگر فقط Orders Feature به آن نیاز دارد، Feature Context مرز خواناتری میدهد و احتمال dependency تصادفی بین featureها را کم میکند.
Encapsulation را با process isolation اشتباه نگیر. این mechanism visibility داخل Fastify را organize میکند؛ OS process، network trust boundary یا microservice نیست. اگر module discipline نداشته باشی، کد همان process هنوز میتواند فایل دیگری را مستقیم import کند. پس architecture هم Fastify Context میخواهد و هم discipline در dependency direction.
برای Evidence دو observation کافی ولی قوی لازم داریم: Orders Route dependency خودش را میبیند؛ Sibling Probe همان feature-local decorator را نمیبیند. این دو مشاهده از یک diagram حفظی معتبرترند.
Dependency Boundary: Collaborator را replace کن، نه کل App را
Feature قابل test یک dependency seam دارد. در Lab، buildApp یک ordersRepo میگیرد و آن را به Orders Feature میدهد. Feature تعیین میکند این repository چگونه در context خودش expose شود. Test یک Memory Repository میسازد؛ composition واقعی بعداً میتواند adapter دیگری بدهد. Handler production singleton را مستقیم import نمیکند.
این الگو سه سؤال را جدا میکند:
- Composition — در این app instance کدام implementation انتخاب شده است؟
- Visibility — کدام Fastify Context مجاز است آن را ببیند؟
- Behavior — Handler واقعاً به چه interface کوچکی نیاز دارد؟
وقتی این سه مورد قاطی شوند، test پرهزینه میشود. Route database client را import میکند، test مجبور میشود database را configure کند و برای replace کردن dependency به module mocking یا environment hack برسد. با dependency boundary صریح، test یک fake deterministic میدهد و رفتار/call record را مستقیم observe میکند.
Shortcut مقابل هم غلط است: برای «testability» همه serviceها را روی root Decorate نکن. این کار فقط module global را به service locator global تبدیل میکند. Narrowest Context را انتخاب کن که intentional sharing را نشان دهد. Authenticator سراسری ممکن است root-visible باشد؛ Orders Repository فقط بهخاطر اینکه چند handler همان feature از آن استفاده میکنند global نمیشود.
Route Schema: Transport Boundary است، نه Business Truth
Fastify از schema-based validation و serialization استفاده میکند. در WEB-005، Request Schema مانع ورود data خراب به handler میشود و Response Schema shape خروجی موفق را محدود میکند. در Lab، body ساخت سفارش باید sku غیرخالی و quantity عدد صحیح مثبت داشته باشد. Request نامعتبر باید قبل از repo.create رد شود.
Schema definition را Application Code در نظر بگیر. یک schema دلخواه را از request untrusted نگیر و compile نکن. Schema باید در source کنترلشده route یا module application باشد.
اما JSON Schema Business Correctness نیست. quantity >= 1 rule شکل transport است. اینکه SKU در منطقه کاربر قابل فروش است یا inventory کافی وجود دارد Business Rule است و ممکن است به service/domain logic بعد از validation نیاز داشته باشد. این distinction عمداً در درس حفظ میشود تا یک validation سبز، proof کل feature تلقی نشود.
Response Schema هم مهم است. Handler ممکن است object repository را با field داخلی برگرداند. Schema پاسخ میتواند public success shape را محدود کند. این کار جای Security Review را نمیگیرد، ولی boundary intended را inspectable میکند.
In-Process Test: Route را بدون Production Port ثابت کن
Guidance رسمی Testing در Fastify از inject() برای fake HTTP request داخل process استفاده میکند. payoff معماری این است که app را بهصورت function بسازی، test dependency بدهی، interface HTTP را exercise کنی و app را close کنی، بدون اینکه listening port شرط اجباری test باشد. این هنوز request lifecycle واقعی Fastify برای route ثبتشده را اجرا میکند؛ فقط socket سیستمعامل تنها seam نیست.
Test خوب در این درس چهار کار میکند:
- برای هر test یک app تازه با Fake Repository deterministic میسازد.
- Request معتبر inject و response و repository observation را assert میکند.
- Input نامعتبر inject و ثابت میکند validation قبل از repository mutation آن را رد کرده است.
- app را close و resource cleanup قابل مشاهده را بررسی میکند.
بعداً میتوان integration/system test با port واقعی داشت، وقتی سؤال واقعاً درباره network است. WEB-005 فقط assumption «هر API test باید listen کند» را رد میکند. Pass شدن inject() درباره reverse proxy، TLS، DNS production یا رفتار kernel socket چیزی ثابت نمیکند.
Lifecycle: Boot و Close بخشی از Testability هستند
اگر route assertion سبز شود ولی resource باز بماند، testability کامل نیست. Fastify lifecycle برای boot و close دارد. Lab ownership را visible میکند: Builder resource را میسازد یا میگیرد، test میداند owner کیست و close() مسیر cleanup resource owned را فعال میکند.
این تشریفات نیست. Suite ای که روی process exit حساب کند میتواند timer، pool یا connection باز را پنهان کند. Plugin با setup async اگر boot آن مشخص نباشد race ایجاد میکند. Test bounded باید بداند app چه زمانی آماده است و بعد از assertion آن را ببندد.
Evidence قویتر از «close را صدا زدیم» است: Fake Repository یک closed flag یا close event دارد و بعد از app.close() تغییر آن observe میشود. این observation به Lifecycle Row معنا میدهد.
Worked Example: Orders Feature را isolate کن
Scenario واقعی سرویس دارای infrastructure مشترک است و ما Orders را بدون تبدیل Root به Orders Service Locator اضافه میکنیم.
buildApp repository را از option میگیرد و Orders Feature را با همان dependency register میکند. داخل Feature Context، plugin ordersRepo را local Decorate میکند، Route و Schema را تعریف میکند و handler از dependency همان context استفاده میکند. یک sibling visibility probe بررسی میکند ordersRepo را میبیند یا نه. چون Decoration feature-local است، جواب sibling باید false باشد.
Reference Test یک Memory Repository میسازد. POST /orders با JSON معتبر inject میشود و 201 با fieldهای public برمیگردد. سپس body با quantity: 0 inject میشود و validation باید آن را رد کند. بعد sibling probe باید نبود decorator را report کند. در پایان app close میشود و state resource بررسی میشود.
Evidence Matrix پنج ردیف دارد:
| Dimension | Contract | Evidence قابل مشاهده | چه چیزی آن را رد میکند؟ |
|---|---|---|---|
| Plugin Context | dependency سفارش feature-local است | Orders route آن را میبیند، sibling نمیبیند | sibling decorator را visible گزارش کند |
| Dependency Boundary | repository در composition قابل جایگزینی است | Fake Test call را ثبت میکند | Test production repo را import کند |
| Route Schema | malformed input قبل از repo رد میشود | validation failure و call count صفر | call count زیاد شود |
| In-Process Test | Route بدون listener قابل اثبات است | inject() response میدهد | تنها seam، listen() باشد |
| Lifecycle | Resource owned cleanup صریح دارد | close flag/event مشاهده شود | Test با resource باز تمام شود |
Table صرفاً documentation نیست؛ Hidden Oracle برای هر row یک mutation دارد.
Failure Mode و Mutation عمدی
Mutation 1 — Decorator را در Sibling بگذار و visibility را فرض کن
قبل از اجرا prediction بده: Orders Route باید dependency را از دست بدهد اگر Decoration فقط در sibling نامرتبط باشد. Diagnosis صحیح «جهت Encapsulation» است، نه «Fastify تصادفی رفتار کرد». Repair یا dependency را در ancestor عمداً مشترک قرار میدهد یا در این درس آن را داخل Orders Feature نگه میدارد.
Mutation 2 — Request Validation را حذف کن
Prediction: input خراب وارد Handler/Repository میشود. Observation فقط status نیست؛ call count fake repository نشان میدهد transport boundary شکسته است. Repair Schema trusted را برمیگرداند. ادعا نکن Business Ruleها هم اثبات شدند.
Mutation 3 — Production Dependency را در Test استفاده کن
Prediction: Test deterministic نیست و به environment وابسته میشود. حتی اگر روی یک سیستم PASS شود، replacement seam را ثابت نمیکند. Repair با Fake Repository و assertion روی behavior/call record انجام میشود.
Mutation 4 — Real Port را تنها Test Boundary بدان
Port واقعی ذاتاً بد نیست. وقتی هدف architectural test کوچک است، بد است که تنها seam باشد. برای route test از inject() استفاده کن و network test را برای سؤالهای واقعی socket/proxy/TLS نگه دار.
Mutation 5 — Cleanup را فراموش کن
Assertion میتواند سبز باشد ولی application باز بماند. Repair با test cleanup و ownership صریح انجام میشود. Evidence باید تغییر state resource را ثبت کند، نه صرفاً وجود line close().
Experienced Bridge: Visibility Matrix را Review کن
Engineer باتجربه میتواند syntax walkthrough را رد کند و مستقیم این Matrix را بررسی کند:
- هر Decorator/Hook در کدام context تعریف شده است؟
- کدام descendantها باید آن را inherit کنند؟
- کدام feature-local dependency نباید sideways leak شود؟
- Route Schema دقیقاً چه چیزی را validate میکند و چه چیزی را نمیکند؟
- کدام Builder Option در test replace میشود؟
- Testها از inject/ready/close چگونه استفاده میکنند؟
- هر resource owner و shutdown path مشخص دارد؟
اگر Matrix نتواند توضیح دهد sibling چرا dependency را میبیند یا نمیبیند، architecture به registration order تصادفی وابسته است. اگر یک collaborator بدون module cache trick قابل تعویض نیست، Dependency Boundary ضعیف است. اگر route validation فقط همراه production database test شود، concernها هنوز coupled هستند.
Alternatives و Trade-offها
Root Decorator یا Feature-local Decorator. Root برای infrastructure عمداً مشترک مناسب است. Feature-local ownership محدودتر را communicate میکند. انتخاب بر اساس intended visibility است، نه قانون مطلق.
Plugin Option Injection یا Direct Import. Plugin Option composition seam را صریح میکند و برای fixture این درس مناسب است. Direct Import برای utility pure/stateless ساده میتواند خوب باشد. External adapter stateful بهصورت singleton coupling بیشتری میسازد.
inject() یا Real Listener. inject() برای رفتار application داخل Fastify سریع و deterministic است. Listener واقعی وقتی لازم است که خود network موضوع test باشد. این دو رقیب مطلق نیستند؛ سؤالهای مختلف را جواب میدهند.
Schema Validation یا Business Validation. JSON Schema برای transport shape مناسب است. Business Ruleهای stateful در service/domain logic میمانند. Database check را داخل initial schema validation نبر؛ concernها را مخلوط میکند.
یک Plugin بزرگ یا Pluginهای زیاد. تعداد بیشتر Plugin بهخودیخود architecture بهتر نیست. Plugin Boundary وقتی ارزش دارد که ownership، visibility، lifecycle یا composition را روشن و testable کند.
Guided Practice و Transfer مستقل
قبل از edit کردن Starter، پنج prediction بنویس: ordersRepo کجا visible است؟ آیا repository قابل replace است؟ malformed input به repo میرسد؟ آیا port اجباری است؟ چه چیزی resource را close میکند؟ بعد کوچکترین repair را انجام بده.
Observation و Inference را جدا ثبت کن. «Sibling Probe مقدار hasOrdersRepo: false داد» Observation است. «Feature-local Decorator sideways leak نکرد» Inference bounded است. «Invalid Payload رد شد و Repo Call Count صفر ماند» Observation است. «Route Schema در این fixture transport boundary را محافظت کرد» Inference است.
برای transfer مستقل، یک Shipments Feature بساز: shipmentStore قابل جایگزینی، Route Schema مستقل، sibling visibility probe و inject-based test. Fieldهای Orders را copy نکن و ordersRepo را در transfer باقی نگذار. هدف انتقال Method است، نه انتقال nounها.
Verification: Fastify Boundary & Test Harness Record v1
Artifact وقتی کامل است که این Evidenceها را داشته باشد:
- implementation fileهایی که Plugin/Context Boundary را نشان میدهند؛
- observation visibility در Owned Route و Sibling Probe؛
- dependency قابل جایگزینی در test و نبود Production Singleton در test؛
- observation request معتبر و نامعتبر متصل به Route Schema؛
- Inject-based response و statement صریح اینکه real-network behavior ثابت نشده؛
- close/resource evidence؛
- Mutation با Prediction، Observation، Diagnosis، Repair و Repair Observation؛
- Shipments Transfer مستقل؛
- separation بین Observation و Inference؛
- حداقل یک Residual Limitation.
Hidden Oracle سبز فقط همین fixture و checkهای encoded را ثابت میکند. درباره Production Networking، Security Architecture، همه Business Ruleها، Database Correctness یا برتری universal یک Plugin Layout ادعا نمیکند.
این درس API-only است و Browser/Assistive-Technology Evidence لازم ندارد و ادعا هم نمیشود.
Assessment و Transfer
- Retrieval: پنج claim family و یک non-goal را بگو.
- Application: Orders Starter را به Feature Plugin با Dependency قابل جایگزینی، Trusted Route Schema و In-Process Test تبدیل کن.
- Diagnosis: یک Context Visibility یا Validation Mutation را پیشبینی، مشاهده و repair کن.
- Decision: انتخاب Root-vs-Feature Dependency و Inject-vs-Listen را بر اساس Evidence Need دفاع کن.
- Transfer: Shipments Feature را مستقل حل کن و یک Residual Limitation بنویس.
پاسخ قوی فقط نمیگوید «Plugin استفاده کن» یا «Dependency Injection داشته باش». Contract و Evidence falsifiable را کنار هم قرار میدهد.
References و Freshness
Fastify Plugins — مستندات رسمی current. در 2026-09-13 دوباره بررسی شد. فقط برای claim محدود register و ایجاد scope پیشفرض و reasoning مربوط به Encapsulation استفاده میشود.
Fastify Encapsulation — مستندات رسمی current. در 2026-09-13 دوباره بررسی شد. برای Visibility Decorator، Hook، Plugin و Route بین Contextها استفاده میشود.
Fastify Validation and Serialization — مستندات رسمی current. در 2026-09-13 دوباره بررسی شد. برای Schema-based Request/Response Boundary و هشدار «Schema = Application Code، نه Untrusted Runtime Data» استفاده میشود.
Publisher Fixture در این checkpoint روی Fastify 5.12.1 pin شده است تا reproducible باشد. Method پایدار درس Evidence مربوط به Context/Dependency/Schema/Test/Lifecycle است. اگر API یا guidance فعلی Fastify تغییر کرد، جزئیات volatile بهروزرسانی میشوند بدون اینکه bounded outcome یا evidence standard بیدلیل گسترش پیدا کند.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
WEB-005-S1 retrieval
پنج claim family و یک non-goal را بگو.
WEB-005-S2 application
Orders Starter را به Feature Plugin با Dependency قابل جایگزینی، Trusted Route Schema و In-Process Test تبدیل کن.
WEB-005-S3 diagnosis
یک Context Visibility یا Validation Mutation را پیشبینی، مشاهده و repair کن.
WEB-005-S4 decision
انتخاب Root-vs-Feature Dependency و Inject-vs-Listen را بر اساس Evidence Need دفاع کن.
WEB-005-S5 transfer
Shipments Feature را مستقل حل کن و یک Residual Limitation بنویس.
ملاکهای ارزیابی (rubric)
Critical dimensions:
- PLUGIN_CONTEXT — Use register/encapsulation deliberately and trace which decorators/hooks/routes are visible in each context.
- DEPENDENCY_BOUNDARY — Inject or decorate dependencies at a controlled boundary so tests can replace them.
- ROUTE_SCHEMA — Validate request inputs and serialize outputs with explicit trusted schemas.
- IN_PROCESS_TEST — Use Fastify injection/ready lifecycle to test the application without binding a production network port.
- LIFECYCLE — Make boot/close and resource ownership test-visible instead of relying on process exit.
Blocking assessments: S2, S3, S5. S1 and S4 are non-blocking but still require scope discipline. Publisher-runtime execution remains deferred to the pinned runtime. Internal synthetic oracle results are not represented as publisher-runtime observation. Browser/assistive-technology evidence is not required for this API-only lesson and is not claimed.
منابع و تازگی
مطالعهٔ موارد (case studies)
فایلهای آزمایشگاه
Starter خراب Orders را به Feature Plugin ایزوله تبدیل کنید. از Fastify pinشده در publisher environment استفاده کنید و outbound network یا database خارجی لازم نکنید. ordersRepo باید feature-local باشد، Route Schema trusted اضافه شود، repository در test قابل replace باشد، bounded API test با inject انجام شود و resource owned بسته شود. Evidence Record و Shipments Transfer مستقل را کامل کنید. Expected Rowهای Hidden در اختیار learner نیستند.
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)