FullStack Course LogoFullStack Course
Module: TypeScript
TypeScript·179·10 MIN READ

179: TypeScript Capstone: Strict Domain Library and Full-Stack Vertical Slice

TOPICS COVERED: TypeScript Capstone: Strict Domain Library and Full-Stack Vertical Slice

Learning outcomes

By the end of this lesson, you should be able to:

  • explain and apply domain vocabulary in a realistic implementation;
  • explain and apply boundary parsing in a realistic implementation;
  • explain and apply public API design in a realistic implementation;
  • explain and apply full-stack inference in a realistic implementation;
  • explain and apply runtime tests and type tests in a realistic implementation.

Prerequisites and retrieval

This lesson assumes that you have the 01–06 foundation and have completed the preceding lessons in this module. Before you begin, retrieve one concrete example from an earlier project where one of these concerns appeared. You are not trying to memorize a list of terms. You are practicing how to make and defend a design decision in a strict TypeScript codebase.

The compiler can help model domain invariants, but it cannot replace runtime validation. Values arriving over HTTP, from a database, from a queue, or from a user are still untrusted until the running program checks them.

Terminology

  • Domain vocabulary: Give model identifiers, finite states, commands, results, and invariants clear named types. Good names make the allowed relationships visible in the code.
  • Boundary parsing: Treat every external input as unknown, or as the broad value supplied by a framework, and make it trusted only after validation. Parsing is the point where uncertainty is reduced.
  • Public API design: Expose small, stable module surfaces, using type-only exports where appropriate. Consumers should depend on deliberate contracts rather than implementation details.
  • Full-stack inference: Let typed API adapters feed React and TanStack Query v5 so component and query types are inferred from validated response contracts instead of being recreated as separate manual interfaces.
  • Tests and type tests: Prove that invalid runtime data is rejected and that invalid calls remain impossible at compile time.
  • Build and migration readiness: Document module resolution, package boundaries, compiler rules, and a path for adopting stricter options or newer TypeScript versions without requiring a “big bang” rewrite.

Mental model

Treat TypeScript Capstone: Strict Domain Library and Full-Stack Vertical Slice as a design problem with observable inputs, outputs, invariants, and failure modes. The capstone is not asking you to hide uncertainty behind type assertions. It asks you to combine runtime parsing, domain modeling, advanced type relationships, framework integration, and tests in a way that makes the boundaries visible.

A strong implementation leaves evidence for its decisions. That evidence might be tests, types, constraints, metrics, or diagrams, but it should make clear why the design is safe and what happens when an assumption fails.

A useful sequence for both production work and technical interviews is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement straight to a library call. First write down what must remain true. Then choose the mechanism that enforces those conditions. That order makes it easier to tell whether a bug belongs to parsing, domain logic, persistence, or presentation.

Deep dive

1. Domain vocabulary

Model identifiers, finite states, commands, results, and invariants with clear named types. Discriminated unions are useful when the available fields depend on a state. Branded IDs are useful when they prevent realistic invalid combinations, such as passing a UserId where a TaskId is required.

Decision rule: Use domain vocabulary deliberately when it makes a contract or invariant easier to prove. If it merely reduces typing while hiding an assumption, prefer the more explicit design.

2. Boundary parsing

Every external input starts as unknown or as a broad framework-provided value. It becomes trusted only after validation has established the properties the next layer needs. Keep these parsers reusable so the same rules can be applied by HTTP handlers, tests, workers, and import jobs rather than reimplemented in each caller.

Decision rule: Use boundary parsing deliberately when it makes a contract or invariant easier to prove. If it merely reduces typing while hiding an assumption, prefer the more explicit design.

3. Public API design

Expose a small, stable module surface, using type-only exports where that is appropriate. Do not make consumers depend on database rows, framework request types, or internal utility types. A public API is a boundary: once other code depends on it, changing an internal representation should not require changing every consumer.

Decision rule: Use public API design deliberately when it makes a contract or invariant easier to prove. If it merely reduces typing while hiding an assumption, prefer the more explicit design.

4. Full-stack inference

Let typed API adapters feed React and TanStack Query v5. When the response contract has already been validated, the component and query code can infer its types from that single contract instead of maintaining a second, potentially stale interface by hand.

Inference is valuable because it reduces drift, not because it eliminates the need to understand the data. If the adapter's validation is wrong, every consumer can still receive a misleading type, so the runtime boundary remains part of the design.

Decision rule: Use full-stack inference deliberately when it makes a contract or invariant easier to prove. If it merely reduces typing while hiding an assumption, prefer the more explicit design.

5. Tests and type tests

Prove both sides of the boundary. Runtime tests should show that invalid data is rejected, while type-level tests should show that invalid calls do not compile. Include a test that would have failed if an assertion lied about the payload; otherwise, a cast can make an unsafe value look safe without any test exposing the mistake.

Decision rule: Use tests and type tests deliberately when it makes a contract or invariant easier to prove. If it merely reduces typing while hiding an assumption, prefer the more explicit design.

6. Build and migration readiness

Document module resolution, package boundaries, compiler rules, and how the project can adopt stricter options or a new TypeScript version without a “big bang” rewrite. Configuration is part of the design: a type-safe library that cannot be built consistently, or whose package boundaries are unclear, is not ready for reliable use.

Decision rule: Use build and migration readiness deliberately when it makes a contract or invariant easier to prove. If it merely reduces typing while hiding an assumption, prefer the more explicit design.

Worked example

Consider a strict TypeScript codebase in which the compiler helps model domain invariants, while runtime validation still protects the program from untrusted data. Start by stating the requirement in one sentence. Then list the input and output contracts, and identify which concept above owns each failure mode.

The useful distinction is separation of concerns. Parsing and validation belong at the boundary. Domain rules belong in the domain or service layer. Persistence rules belong in the database or repository. Presentation rules belong in the client. Combining those responsibilities can make a happy-path demo shorter, but it makes edge cases and ownership much harder to reason about.

For example, a branded ID can prevent accidental mixing of identifiers after parsing:

ts
type TaskId = string & { readonly __brand: 'TaskId' };

function parseTaskId(value: unknown): TaskId {
  if (typeof value !== 'string' || value.length === 0) {
    throw new TypeError('Invalid task id');
  }
  return value as TaskId;
}

The assertion on the final line does not validate anything by itself. The preceding check is what justifies the narrower type for this particular function. The brand is also a compile-time distinction only; it does not change the runtime representation, so any value entering the system elsewhere still needs to pass through an appropriate parser.

Walk this example through at least four cases: the normal path; an empty or missing value; a duplicate, retry, or concurrent path where that concern applies; and a dependency failure. For each case, state which layer detects the problem and what the caller observes. That is the level of explanation expected in a senior code review or technical interview: not just that the code works for valid input, but why each failure has a clear owner and observable result.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failures, stale-client requests, concurrent requests, malformed data, schema changes, and high-cardinality workloads. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after evidence identifies a bottleneck or a material risk.

When an external dependency is involved, define timeout and cancellation behavior. When persistence is involved, define transaction and consistency expectations. When the feature has user-visible state, account for loading, empty, error, stale, and success states. When security is involved, assume the client can be modified and every network input is untrusted; client-side types and validation do not authorize a request.

Guided lab

Build a strict TypeScript task/order vertical slice that includes runtime schemas, branded IDs, discriminated domain state, repository/service/HTTP boundaries, a React form, TanStack Query v5 integration, runtime tests, type-level tests, and a short architecture note.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements. Non-requirements keep the first slice from quietly expanding into an unrelated system.
  2. List the input, output, and error contracts before implementation.
  3. Implement the smallest correct vertical slice.
  4. Add at least one invalid-input test and one edge-case test.
  5. Instrument or inspect the behavior instead of guessing.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration option.
  7. Explain one alternative design and why you did not choose it.
  8. Record a short “what would break at 10× scale?” note.

The lab should leave you with more than a happy-path request. You should be able to point to the boundary that rejects malformed data, the layer that owns the domain rule, the contract consumed by the client, and the tests that would expose a false assumption.

Edge cases and failure modes

  • Domain vocabulary: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Boundary parsing: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Public API design: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Full-stack inference: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Tests and type tests: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes.

These cases are prompts to connect the type model to actual behavior. For instance, a type may prevent two ID categories from being mixed at compile time, while a duplicate request still requires a runtime policy such as idempotency or a database constraint.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied pattern can be syntactically correct while still being architecturally wrong for the actual contract.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values.
  • Testing only the happy path and discovering the real contract only after integration.
  • Optimizing before measuring, or selecting a scalable mechanism without an actual scale requirement.
  • Allowing client-side behavior to stand in for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case first. Inspect the actual value or execution plan, trace the boundary where the invariant first became false, and fix the layer that owns the problem rather than adding a downstream patch. Check the source and build when a type or module behaves unexpectedly, the HTTP or Network boundary when the payload differs, the server or route when the response is wrong, the database or query when persistence is involved, and deployment or configuration when behavior changes between environments.

Interview questions

  1. What problem does Domain vocabulary solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem does Boundary parsing solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem does Public API design solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does Full-stack inference solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem do Tests and type tests solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain TypeScript Capstone: Strict Domain Library and Full-Stack Vertical Slice to another developer in five minutes. Include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or under stricter reliability requirements.

References

Reader page: /typescript/lesson/179/typescript-capstone-strict-domain-library-and-full-stack-vertical-slice