وب مدرن، رابط کاربری و تجربه‌ی وب · فصل ۵ — Error Model، REST، Streaming و Webhook · 49

Fastify Architecture and Testable APIs

Structure a bounded Fastify feature around explicit plugin/context boundaries, schemas and injectable dependencies, then prove the API through in-process request tests rather than only a live server. می‌تواند «Fastify Architecture و Testable API» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

Fastify Architecture و Testable API — fa-IR

Lesson summary

Structure a bounded Fastify feature around explicit plugin/context boundaries, schemas and injectable dependencies, then prove the API through in-process request tests rather than only a live server.

می‌تواند «Fastify Architecture و Testable API» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

This lesson produces

می‌تواند «Fastify Architecture و Testable API» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

Who it is for

Foundation

Worked baseline, explicit evidence table, safe local fixture and one guided failure probe.

Repair

Reduce to one failing contract, observe evidence, repair, then restore full case.

Experienced

Skip syntax walkthrough; require independent implementation, mutation evidence and residual-risk statement.

Prerequisites

A service can pass the happy path and still have the wrong boundaries

A Fastify service already has authentication and database infrastructure. A developer adds an orders route directly on the root instance, imports a production repository inside the handler, accepts an unvalidated body, starts a real network port in the test, and leaves cleanup to process exit. The endpoint can return 201 in a demo. That result proves very little about the architecture. A sibling feature may accidentally depend on the orders repository, a test may touch production-like resources, malformed input may reach business code, and a hanging resource may be hidden by the test runner.

WEB-005 treats Fastify architecture as an evidence problem. The unit of reasoning is not “where should this file live?” but “which Fastify context owns this dependency, which descendants can see it, what schema protects the route boundary, how can the dependency be replaced in a test, and what lifecycle action proves cleanup?” The output is Fastify Boundary & Test Harness Record v1, a row-by-row record of context visibility, dependency ownership, route schema behavior, in-process injection, lifecycle evidence, one failure mutation, repair evidence, and an independent transfer.

The five owned claim families are Plugin Context, Dependency Boundary, Route Schema, In-Process Test, and Lifecycle. They belong together because a feature that is isolated but not testable is incomplete, while a test that passes only because every dependency is global does not prove a useful boundary.

Route orientation and scope

This lesson assumes the error-model distinctions from WEB-004. It does not redesign public error taxonomy or retryability. WEB-006 owns deeper REST contract design, idempotency and pagination. WEB-005 owns a smaller architectural question: how to place a Fastify feature in an encapsulation context, give it explicit dependencies, validate and serialize its route boundary, and test it without making a production listener the default test seam.

Choose the foundation route if register, decorators and encapsulation are still mostly syntax to you. Choose the repair route if you already use plugins but have hidden imports, global decorators or tests that boot real infrastructure. Choose the experienced route if you can skip basic API walkthroughs and go straight to context visibility, dependency replacement, lifecycle ownership and adversarial mutations. All routes end at the same evidence bar.

This lesson does not claim that a Fastify plugin is a microservice, security sandbox or deployment boundary. It does not claim every dependency belongs on the root instance. It does not claim inject() proves reverse-proxy, TLS or real socket behavior. It does not claim JSON Schema proves business correctness. And it does not claim Fastify is required to build a testable Node.js service.

Mental model: context graph before folder tree

Fastify's useful mental model is a graph of contexts. The root instance can register a feature plugin. Registration creates a child scope by default. Decorations, hooks, plugins and routes follow encapsulation visibility: descendants can inherit from ancestors, while a child does not push its local decorations back into the ancestor or sideways into a sibling. That direction matters more than whether two files happen to be adjacent in a repository.

Use this sequence:

  1. Root context owns truly application-wide concerns and composition.
  2. Feature plugin creates the boundary for one bounded feature such as orders.
  3. Decorator/dependency visibility makes the feature's required collaborator explicit in the context that owns its routes.
  4. Route schema constrains incoming data and the serialized output shape.
  5. Handler uses the dependency through the controlled boundary instead of reaching into hidden global state.
  6. Inject test sends a request in process and observes status/body/headers without requiring a production port.
  7. Lifecycle evidence proves boot and close behavior, especially when resources are owned by the built application.

The architecture is therefore not “plugins everywhere.” A root-level dependency can be appropriate when multiple independent features intentionally share it. A feature-local dependency is appropriate when ownership and visibility should stop at that feature. The decision must be explicit enough that a test can replace the dependency and a mutation can prove who can see it.

Foundation layer: register and Plugin Context

Fastify documentation describes register as the mechanism for adding plugins and states that registration creates a new scope by default. That scope is the basis of Fastify encapsulation. The practical consequence is directional visibility. If the orders feature decorates its local Fastify instance with ordersRepo, routes inside that feature can use it. The root context does not automatically gain that decoration, and an independent sibling plugin should not assume it exists.

That behavior is easy to misunderstand because a small application often starts with everything on the root instance. Root decoration makes a dependency visible to every descendant, which can feel convenient. But convenience is not the same as a good ownership boundary. If only the orders feature needs ordersRepo, placing it in the feature context makes accidental cross-feature access harder and makes the intended dependency graph easier to inspect.

Do not equate this with process isolation. Encapsulation organizes Fastify visibility; it is not a separate operating-system process, network trust boundary or microservice. Code in the same process can still import modules directly if you let it. Architecture therefore needs both a Fastify context boundary and module discipline.

A useful context test asks two questions. First: can an orders route use the dependency that its feature explicitly owns? Second: can a sibling probe see that same feature-local decorator? The expected answer is yes, then no. Recording both observations is stronger evidence than drawing a plugin tree from memory.

Dependency Boundary: replace the collaborator, not the whole app

A testable feature has a dependency seam. In the lab, the application builder receives an ordersRepo. It passes that collaborator into the orders feature as plugin options. The feature owns how the repository is exposed to its own handlers. Tests can provide a memory repository; production composition can provide a real adapter later. The handler does not import a production singleton.

This pattern separates three questions:

  • composition — which concrete repository is chosen for this application instance;
  • visibility — which Fastify context is allowed to use it;
  • behavior — what interface the handler actually needs.

When those concerns are collapsed, a test often becomes expensive. A route imports a database client, the test must configure that database, and replacing the collaborator requires module mocking or environment tricks. With an explicit dependency boundary, the test can create a small fake with deterministic behavior and inspect calls directly.

The opposite extreme is also wrong: decorating every possible dependency on the root “for testability.” That merely replaces hidden module globals with a global service locator. Prefer the narrowest context that matches intentional sharing. A cross-cutting authenticator may legitimately be root-visible; an orders repository does not become global just because several route handlers inside the orders feature need it.

Route Schema: transport validation is a boundary, not business truth

Fastify uses schema-based validation and serialization. For WEB-005, schemas do two jobs. The request schema prevents malformed transport data from reaching the handler, and the response schema constrains what the route is allowed to serialize for the documented success shape. In the lab, an order-creation body requires a non-empty SKU and a positive integer quantity. A malformed request should fail before repository creation is called.

Schemas must be treated as application code. Do not accept an arbitrary schema from an untrusted request and compile it as if it were data. The trusted schema belongs with the route contract or another controlled application module.

Schema validation is not business correctness. quantity >= 1 is a transport-level shape rule. “This SKU may be purchased in this region” is a business rule and may require services or domain logic after validation. The lesson deliberately keeps that distinction visible so a passing schema check does not become a false proof of the whole feature.

Response serialization deserves the same discipline. A handler can accidentally return an internal field that happens to exist on a repository object. An explicit response schema can limit the public success shape. That still does not replace a security review, but it makes the intended boundary inspectable.

In-Process Test: prove the route without making the network the default seam

Fastify's testing guidance supports inject() for fake HTTP requests inside the process. The important architectural payoff is that the application can be built as a function, configured with test dependencies, exercised through its HTTP interface, and closed without a required listening port. The test still observes a real Fastify request lifecycle for the registered route; it simply avoids treating an operating-system socket as the only entry point.

A good test in this lesson does four things:

  1. build a fresh application instance with a deterministic fake repository;
  2. inject a valid request and assert the response plus repository observation;
  3. inject malformed input and assert that validation rejects it before repository mutation;
  4. close the application and verify owned resource cleanup.

A separate integration/system test may later bind a real port when real-network behavior matters. WEB-005 only rejects the assumption that every API test must do so. An inject() pass cannot tell you whether a reverse proxy rewrites headers, whether TLS is configured correctly, whether the kernel socket backlog behaves as expected, or whether production DNS is correct. Those are different evidence questions.

Lifecycle: boot and close are part of testability

Testability is not complete if the route assertion passes while resources leak. Fastify has an application/plugin boot lifecycle and a close lifecycle. The lab makes ownership visible: the builder either creates or receives a repository-like resource, the test knows who owns it, and close() triggers the cleanup path for owned resources.

This is not ceremony. A suite that relies on process exit can hide leaked timers, pools and connections. A feature that registers asynchronous setup but is never awaited can create race conditions. A bounded test should know when the application is ready to accept injected requests and should close the application after the assertion.

The strongest evidence is not “we called close().” It is an observable resource state: the fake repository's closed flag changed, or an onClose hook recorded one close event. That gives the lifecycle row in the evidence matrix something concrete to cite.

Worked example: isolate an orders feature

The realistic scenario starts with a service that already has shared auth/database infrastructure. We add orders without turning the root into an orders-specific service locator.

The application builder receives a repository option. It registers the orders feature with that repository. Inside the feature context, the plugin decorates the feature instance with ordersRepo, defines the order route and its schemas, and handles the request through that dependency. A sibling visibility-probe plugin reports whether it can see ordersRepo. Because the decoration is feature-local, the sibling should report false.

The reference test creates a memory repository. It injects POST /orders with valid JSON and expects 201 plus the public order fields. It injects a body with quantity: 0 and expects Fastify validation to reject it. It also injects the sibling probe and expects the feature-local decorator to remain invisible there. Finally, it closes the app and checks the owned fake resource state.

This produces five evidence rows:

DimensionContractObservable evidenceWhat would falsify it?
Plugin Contextorders dependency is feature-localorders route sees repo; sibling does notsibling reports decorator visible
Dependency Boundaryrepository is replaceable at compositiontest fake receives create calltest imports production repository
Route Schemamalformed transport input is rejectedinvalid body returns validation failure before repo callrepo call count increases
In-Process Testroute can be proven without a listening portinject() yields responsetest requires listen() as only seam
Lifecycleowned resource has explicit close evidenceclose event/flag observedtest exits with resource still open

The table is not just documentation. Each row has a deliberate mutation in the hidden validator.

Failure modes and deliberate probes

Failure 1 — move the decorator to a sibling and assume visibility

Predict first: the orders route should lose access if its dependency is decorated only in an unrelated sibling context. A correct diagnosis names encapsulation direction, not “Fastify is flaky.” Repair by putting the dependency in an ancestor intentionally shared by both contexts or, for this lesson, inside the orders feature that owns the route.

Failure 2 — remove request validation

Predict: malformed input will reach the handler/repository. The observation is not merely a different status code; the fake repository call count becomes evidence that the transport boundary failed. Repair by restoring the trusted route schema. Do not claim that the schema now proves stock or authorization rules.

Failure 3 — reuse the production dependency in the test

Predict: the test stops being deterministic and may require environment configuration. Even if it passes on one machine, it no longer proves the explicit replacement seam. Repair by passing a fake repository to the builder and asserting through its public behavior/call record.

Failure 4 — bind a real network port and mistake that for the only test boundary

A real port is not inherently wrong. It is wrong as the only seam when the goal is a small architectural test. Repair by using inject() for bounded route tests and reserve real-network tests for questions that actually involve sockets, proxies, TLS or external routing.

Failure 5 — forget cleanup

The test can pass while the application remains open. Repair by registering cleanup with the test harness and by making resource ownership visible. The evidence row should include what changed on close, not only that a line of code called close().

Experienced bridge: review the visibility matrix

For an experienced engineer, skip the syntax tour and review the service as a matrix:

  • context that defines each decorator/hook;
  • contexts expected to inherit it;
  • feature-local dependencies that must not escape sideways;
  • route schemas and what they do not validate;
  • application-builder options that tests can replace;
  • tests using inject/ready/close;
  • resources with an explicit owner and shutdown path.

If the matrix cannot explain why a sibling can or cannot see a dependency, the architecture is relying on accidental registration order. If the test cannot replace one collaborator without module cache tricks, the dependency boundary is weak. If route validation cannot be tested independently of a production database, concerns are still coupled.

Alternatives and trade-offs

Root decorator vs feature-local decorator. Root placement is appropriate for intentionally shared infrastructure. Feature-local placement communicates narrower ownership. The choice should follow intended visibility, not a rule that one level is always superior.

Plugin option injection vs direct module import. Plugin options make the composition seam explicit and work well for a bounded fixture. Direct imports are simpler for pure stateless utilities. A stateful external adapter imported as a singleton creates stronger coupling and makes replacement harder.

inject() vs real listener. inject() is fast and deterministic for application behavior inside Fastify. A real listener is required when the thing under test is networking behavior itself. One does not invalidate the other; they answer different questions.

Schema validation vs service/business validation. JSON Schema is ideal for many transport-shape constraints. Business rules may depend on state and should remain in business/service logic. Moving database checks into initial schema validation would mix concerns and can create operational risk.

One large application plugin vs many tiny plugins. More plugins do not automatically mean better architecture. Use a plugin boundary when it expresses ownership, visibility, lifecycle or composition that you can explain and test.

Guided practice and independent transfer

Start by reading the broken starter before changing it. Write five predictions: where ordersRepo is visible, whether the repository can be replaced, whether malformed input reaches the repo, whether a port is required, and what closes the resource. Only then implement the smallest repair.

Record observations separately from inferences. “Sibling probe returned hasOrdersRepo: false” is an observation. “The feature-local decorator did not leak sideways through the context graph” is an inference. “Invalid payload returned a validation response and repository call count stayed zero” is an observation. “The route schema protected the transport boundary in this fixture” is the bounded inference.

For independent transfer, build a shipments feature with a replaceable shipmentStore, its own route schema, a sibling visibility probe and an inject-based test. Do not copy the orders public fields or leave ordersRepo in the transferred code. The purpose is to transfer the boundary method, not the nouns.

Verification: Fastify Boundary & Test Harness Record v1

The artifact is complete only when it contains all five dimensions with concrete evidence:

  • implementation files that show the plugin/context boundary;
  • observed visibility from the owned route and a sibling probe;
  • a replaceable dependency used by the test rather than a production singleton;
  • valid and malformed request observations tied to the route schema;
  • an inject-based response observation and the statement that it does not prove real-network behavior;
  • close/resource evidence;
  • one deliberate mutation with prediction, observation, diagnosis, repair and repair observation;
  • an independent shipments transfer;
  • observation/inference separation;
  • one residual limitation.

A green hidden oracle proves only the bounded fixture and encoded checks. It does not prove production networking, security architecture, every business rule, database correctness or the universal superiority of one plugin layout.

No browser or assistive-technology evidence is required for this API-only lesson, and none is claimed.

Assessment and transfer

  1. Retrieval: state the five claim families and one explicit non-goal.
  2. Application: refactor the orders starter into a feature plugin with replaceable dependency, trusted route schema and in-process tests.
  3. Diagnosis: predict and repair a context-visibility or validation mutation.
  4. Decision: defend a root-vs-feature dependency placement and an inject-vs-listen test choice using evidence needs.
  5. Transfer: solve the shipments feature independently and state one residual limitation.

A strong answer names both the contract and the evidence that would falsify it. A weak answer says only “use plugins” or “use dependency injection.”

References and freshness

Fastify Plugins — current official documentation. Rechecked 2026-09-13. Used only for the bounded claim that register adds plugins and creates a new scope by default, supporting encapsulation/inheritance reasoning.

Fastify Encapsulation — current official documentation. Rechecked 2026-09-13. Used only for visibility of decorators, hooks, plugins and routes across Fastify contexts.

Fastify Validation and Serialization — current official documentation. Rechecked 2026-09-13. Used for schema-based request/response boundaries and the warning that schemas are application code rather than untrusted runtime data.

The publisher fixture is pinned to Fastify 5.12.1 for reproducibility at this checkpoint. The stable lesson method is context/dependency/schema/test/lifecycle evidence. If Fastify's current APIs or guidance change, update the volatile implementation details while preserving the bounded outcome and evidence standard.

Assessment

There is no answer key in this lesson pack; answer it yourself and mark honestly. "Needs review" sends the lesson to your review queue.

WEB-005-S1 retrieval

State the five WEB-005 claim families and one explicit non-goal.

lesson ✗AI ✗notes ✗

WEB-005-S2 application

Refactor the orders fixture into an encapsulated feature with replaceable dependency, trusted route schema and in-process test evidence.

lesson ✗AI ✗notes ✗

WEB-005-S3 diagnosis

Predict, run, diagnose and repair one visibility, validation, dependency or lifecycle mutation.

lesson ✗AI ✗notes ✗

WEB-005-S4 decision

Defend root-vs-feature dependency placement and inject-vs-listen testing for the bounded evidence need.

lesson ✗AI ✗notes ✗

WEB-005-S5 transfer

Independently implement the shipments changed-context case without copying orders-specific names or conclusions.

lesson ✗AI ✗notes ✗
Assessment rubric

Critical dimensions:

  1. PLUGIN_CONTEXT — Use register/encapsulation deliberately and trace which decorators/hooks/routes are visible in each context.
  2. DEPENDENCY_BOUNDARY — Inject or decorate dependencies at a controlled boundary so tests can replace them.
  3. ROUTE_SCHEMA — Validate request inputs and serialize outputs with explicit trusted schemas.
  4. IN_PROCESS_TEST — Use Fastify injection/ready lifecycle to test the application without binding a production network port.
  5. 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.

References & freshness

Fastify Plugins — latest
· TOOL_VOLATILE
register adds plugins and creates a new scope by default; use only for bounded plugin-scope/encapsulation reasoning.
Fastify Encapsulation — latest
· TOOL_VOLATILE
Encapsulation context governs visibility of decorators, hooks, plugins and routes; do not treat it as a process/security boundary.
Fastify Validation and Serialization — latest
· TOOL_VOLATILE
Fastify uses schema-based validation/serialization and treats schema definitions as application code, not untrusted data.

Case studies

Lab files

Refactor the broken orders starter into an encapsulated Fastify feature. Use the pinned Fastify dependency already supplied by the publisher environment. Do not require outbound network or a real external database. Keep ordersRepo feature-local, add trusted route schemas, replace the repository in tests, use in-process injection for the bounded API test, and close owned resources. Complete the evidence record and the independent shipments transfer. Hidden expected rows are not included.

Lesson media & slides

MAKNOW_WEB-005_DECISION_EVIDENCE_TABLE_v1.0.0.svg
DECISION EVIDENCE TABLE v1.0.0.svg
MAKNOW_WEB-005_FAILURE_REPAIR_v1.0.0.svg
FAILURE REPAIR v1.0.0.svg
MAKNOW_WEB-005_PRIMARY_MODEL_v1.0.0.svg
PRIMARY MODEL v1.0.0.svg
MAKNOW_WEB-005_WORKED_EXAMPLE_v1.0.0.svg
WORKED EXAMPLE v1.0.0.svg

Teaching files (download)

XLSXFastify Boundary Test Harness Record v1 v1.0.0.xlsxTeaching files (download)
DOCXFastify Boundary Test Harness Record v1 WORKSHEET v1.0.0.docxTeaching files (download)
PDFFastify Boundary Test Harness Record v1 WORKSHEET v1.0.0.pdfTeaching files (download)