TypeScript and API design
Boundary Parsing in TypeScript: Keep External APIs From Becoming Internal Types
Treat every request, webhook, and provider response as unknown until a small boundary parser turns it into an explicit domain value.
A TypeScript annotation is useful documentation for application code, but it does not prove anything about data that has just arrived over the network. A request body, webhook, queue message, browser form, or provider response is data first and a domain value only after the application has checked it. Treating those two states as identical is how a helpful type system becomes a collection of optimistic casts.
Boundary parsing is the small, deliberate step that keeps the distinction visible. It accepts an unknown transport value, checks the parts the application relies on, and returns either a well-defined internal value or a controlled failure. The goal is not a grand validation framework. The goal is to make each crossing from an external contract into application logic explicit, testable, and easy to change.
That framing is particularly useful when a service combines its own API with payments, membership platforms, content systems, scheduled work, and real-time events. The supplied portfolio descriptions include products with token-scoped APIs, recurring-profile state, Socket.IO, Redis timing, and third-party providers. In every setting, a boundary is a useful place to decide what the product recognizes, what it rejects, and what it records for investigation.
The boundary is where types stop being facts
Inside a focused module, a type can be a dependable statement: a transition has been authorized, a selected plan is supported, or a job has a known payload. At the edge of that module, the same type is only an intention. JSON parsing produces JavaScript values, not verified TypeScript objects. Declaring that a response is a Customer changes the compiler's view without examining a single byte of the response.
That distinction matters even when the other side is maintained by the same team. Deployments can be staggered. A worker may retry an older payload. A provider can add a field, omit a field, change a nullable value, or send a newly documented event. An internal client can be buggy or compromised. The more integrations a product has, the more expensive it becomes to discover differences only after unchecked values have reached business logic.
A useful rule is simple: values are unknown at a trust boundary; they become domain values after a parser succeeds. This does not mean every function must receive unknown. It means unknown should be contained at the point where data enters. Domain services should not need to know whether a value came from HTTP, a queue, a webhook, or a persisted retry record. They should receive a shape that has already met the rules needed for the decision they are about to make.
The TypeScript Handbook's explanation of narrowing is relevant because narrowing follows runtime checks. A condition is not just a way to satisfy the compiler; it is evidence that makes a later operation safe. When the runtime check and the type guard describe the same rule, readers can see the contract in one place.
Start with unknown and narrow deliberately
Unknown is a better starting point than any for data that has not been inspected. It accepts every incoming value, but it does not permit property access, calls, or assignment to a narrower type until the code supplies evidence. That friction is helpful at an API edge. It stops a parser from quietly assuming that a nested field exists because an interface says it should.
A parser can begin with deliberately boring checks. First establish that the value is a non-null object. Then establish that a required property is present. Then check its primitive type, allowed values, and rules essential for this operation. Arrays need their own check because they are objects in JavaScript. Null needs its own check because its typeof result is also object. These details are not ceremony; they are the difference between a useful guard and a false sense of safety.
Prefer checks that communicate the actual contract. A truthiness check is often too broad for identifiers, amounts, and optional text because it conflates absence with legitimate empty or zero values. An exact null or undefined check preserves the distinction. A string check can be followed by trimming or an allowed-value lookup when the operation requires it. A number check can be followed by a finite-number and range rule when a calculation depends on those properties.
TypeScript's control-flow analysis can follow these checks through branches and early returns. That makes parsers easier to read when they return as soon as a required condition fails. The successful path then contains only the values that survived each check. Avoid an as assertion as a shortcut around this work. An assertion has no runtime effect, so it is appropriate only after a check the compiler cannot express, not as a replacement for a check.
The strict compiler-option family supports this discipline by surfacing classes of assumptions that permissive settings allow to pass. Strictness cannot validate a webhook or a JSON document for you, but it makes it harder for unchecked uncertainty to spread through ordinary code.
Separate transport shapes from domain values
Transport data should describe what arrived. Domain data should describe what the application is prepared to use. Those are often related but rarely identical. A payment provider may call a status field active, include several optional timestamps, and use strings for identifiers. A domain operation may need only a validated subscription reference, a normalized lifecycle state, and a received-at time. Giving the provider's raw response type to every downstream function turns a provider representation into an accidental internal API.
Create a transport parser near the adapter that owns that provider or protocol. Its input is unknown and its output contains only the fields required by the next layer. The adapter can preserve the original payload separately when a security and retention policy permits it, but it should not force every caller to understand it. A small internal value also makes tests shorter because a test can construct the domain input without reproducing an entire external response.
The supplied EZWxBrief project description mentions Next.js membership flows, NestJS services, PostgreSQL, recurring Payflow profile state, operational reconciliation, and direct email delivery. That is a representative situation for keeping a provider lifecycle representation distinct from an application membership decision. A parser can translate an accepted external state into a named internal outcome, while a domain service decides what trial, renewal, cancellation, reactivation, or payment maintenance means for the product. The parser does not need to make every business decision; it needs to prevent unrecognized transport data from silently making one.
This separation also protects refactoring. If a field is renamed by a provider, the adapter becomes the obvious place to update. If the application changes its own concept of an active membership, the domain type changes without requiring every caller to know provider terminology. The boundary is therefore a maintenance seam, not merely a validation seam.
Model expected outcomes as discriminated unions
A parser should not treat every non-success as an exception. Many outcomes are expected and useful: an unsupported event type, a stale version, a missing optional enrichment, or a valid payload that the current product does not handle. Encode these outcomes so callers must make a decision rather than accidentally continuing with partial data.
A discriminated union provides a practical shape for that choice. Each case has a stable literal discriminator such as accepted, ignored, or invalid. The accepted case carries the parsed domain value. The ignored case can carry a reason code that is safe to count. The invalid case can carry a concise operational code, not a raw provider payload or personal data. A switch over the discriminator makes the handling visible, and an exhaustive default can cause a compile-time failure when a new case is added but not considered.
The TypeScript documentation presents unions as a way to compose possibilities and narrowing as the technique for working with the current possibility. Applied at a boundary, that means a caller cannot accidentally use a value from an invalid branch as though it were accepted. It also means the product can distinguish “we deliberately ignored this supported-but-irrelevant event” from “the payload did not meet our contract.”
Do not over-model every possible property of an external system. Model the outcomes that change what the application does next. A parser that returns dozens of optional fields may be technically typed yet still force every caller to repeat the same defensive decisions. An explicit union keeps the policy near the evidence.
Keep validators small, local, and observable
Large, generic validators can obscure ownership. A boundary parser should be close to the route handler, worker consumer, webhook adapter, or client SDK wrapper that understands the protocol. The code should answer a few concrete questions: What input does this adapter accept? Which facts does the next layer require? Which failures are expected? Which failure codes are safe to observe?
Small parsers are also easier to evolve. Add a new accepted event case in the adapter, add a parser test, then decide whether a domain service needs a new outcome. If the change is wrong, the blast radius is limited. A universal validator often encourages a different mistake: accepting a broad object once and assuming every later use is justified.
Observability should follow the same boundary. Record narrow counters or safe reason codes for rejected schemas, unknown event names, and stale versions. Do not log full request bodies, credentials, or personal fields simply because parsing failed. The useful question in an incident is usually whether a category of contract mismatch increased, not what a particular customer submitted. Keep enough context to correlate operational work without turning a validation path into a data-retention system.
The supplied JSleeve description includes REST and Socket.IO sharing ingestion and lifecycle services, MongoDB for durable state, Redis for live timing, and scheduled finalization. When more than one entry point reaches the same lifecycle, a shared domain parser can be helpful after each transport adapter has performed protocol-specific checks. That arrangement avoids duplicating product rules while still recognizing that a socket payload and a REST request are different external contracts.
Evolve contracts through addition before replacement
Contract changes are easiest when old and new forms can be parsed deliberately for a bounded period. Add a version or a new event name when the meaning changes. Keep the parser able to recognize the older form, translate both forms into the same domain value where that is truthful, and make the removal condition explicit. A hidden fallback that accepts anything is not compatibility; it is deferred uncertainty.
For optional fields, decide whether absence means an older sender, an intentionally omitted value, or an invalid request. Those are different product semantics. For enums, decide whether an unknown future value should be ignored, rejected, quarantined, or passed to a manual review flow. The decision should be based on risk. An unrecognized analytics label may be safely ignored. An unrecognized payment transition should not unlock an account.
Additive evolution also works inside a team. A server can return a new field while older clients ignore it, provided the existing contract stays valid. A client can begin accepting a new response case before the server emits it. Boundary parsers make these rollouts concrete: the parser is the exact place to show which versions and variants are understood today.
Avoid making a type wider merely to silence a deployment mismatch. A wide union without handling code shifts the decision onto every consumer. Prefer a controlled adapter that converts the known compatible cases into the stable domain representation and visibly routes everything else to an appropriate outcome.
Apply the pattern to asynchronous integrations
Asynchronous systems amplify boundary mistakes because the input may be delayed, duplicated, reordered, or retried. Parsing is not idempotency, authorization, or state-transition validation, but it makes those next checks possible on reliable inputs. First recognize the event and normalize the identifiers. Then verify authenticity or authorization where the protocol requires it. Then use the parsed domain command with the idempotency and transition controls owned by the domain layer.
This ordering prevents an adapter from inventing business state while it is still trying to understand a payload. It also keeps retry behavior sensible. A malformed message can receive a controlled non-retryable result or a safe dead-letter route. A recognized message that cannot be processed because a dependency is unavailable can be retried. A duplicate recognized message can be acknowledged by the idempotency layer. Those outcomes are operationally different and should not be folded into one broad catch block.
The supplied Tech Career Assessment description includes role-aware Next.js clients, token-scoped Strapi APIs, PostgreSQL content models, provider abstractions, protected SCORM delivery, transactional email, exports, and migration tooling. In a system with several external and asynchronous paths, a boundary inventory is often more useful than a single API validation ticket. List each route, webhook, worker input, import, and callback; identify its owner; then give it a narrow parser and an explicit outcome policy.
Test the parser, not only the happy path
A boundary parser earns its place through focused tests. Start with one representative accepted payload and assert the exact domain value it returns. Then add failures for missing required properties, wrong primitive types, null where an object is required, unsupported discriminators, and out-of-range values that would affect a business decision. Include examples that prove empty strings and zero values are handled according to the contract rather than accidentally discarded by truthiness.
Test compatibility intentionally. If an old version remains supported, keep a fixture for it and assert that it normalizes to the same internal meaning as the current version. If an unknown future enum must be ignored, assert the ignored outcome and its safe reason code. If an invalid payload must never reach the domain service, make that assertion direct.
Property-based or fuzz testing can be valuable for parsers that consume complex nested structures, but it is not required to begin. A small set of examples tied to known failure modes is often enough to establish the contract. The important part is that parser tests verify runtime behavior. Compile-time tests alone cannot reveal whether a JSON value has the shape the application assumes.
A practical adoption sequence
Begin with one high-change, high-impact boundary rather than rewriting every endpoint. Webhooks, provider callbacks, imports, and background-job messages are good candidates because their inputs are less under direct application control. Write down the domain decision that follows the boundary, define the smallest internal value needed for that decision, and add a parser that returns accepted, ignored, or invalid outcomes.
Next, move raw transport types out of downstream services and add tests for observed failure modes. Add safe reason-code monitoring so the team can see whether real inputs are being rejected. Only then consider shared utilities, and share only the checks that genuinely have the same meaning across adapters.
The payoff is modest code with a large clarity benefit: external change is absorbed at a visible edge, domain logic receives values it can trust, and TypeScript types once again describe evidence rather than hope.
Primary sources
- 1.Narrowing — TypeScript
- 2.Everyday Types — TypeScript
- 3.TSConfig Option: strict — TypeScript
Portfolio evidence
Tech Career Assessment
Connected assessment results to administration, learning delivery, program reporting, and assisted career workflows for internal and external users.
View case studyEZWxBrief
Implemented the recurring-billing and communication capabilities needed to support trial, renewal, cancellation, reactivation, and payment maintenance workflows.
View case studyJSleeve
Established reusable real-time services for personalized shot analysis and reliable competitive game progression.
View case studyRelated writing