One contract, two languages, zero guesswork


Introduction

On most blockchain projects, the TypeScript frontend and the Kotlin/Spring backend drift apart faster than the chain evolves.

Someone tweaks a DTO, regenerates the OpenAPI spec, forgets to update the client, and suddenly the DEX UI interprets a fee field as a value, or mixes testnet and mainnet chains. End‑to‑end type safety is about putting a single contract in the middle and letting both sides compile against it.

Here’s how I approach it: OpenAPI as the source of truth, code generation into Kotlin and TypeScript, plus contract tests to make sure reality still matches the spec.


Architecture: one contract in the middle

The mental model is simple:

+-----------------+        HTTP/JSON        +------------------+
|  TypeScript UI  |  <------------------->  | Kotlin Backend   |
|  (React, dApp)  |                        | (Spring/Ktor)    |
+--------+--------+                         +--------+--------+
         ^                                            ^
         |                                            |
         |            Shared API Contract             |
         +------------------ OpenAPI -----------------+

The OpenAPI document defines:

  • paths, query params and bodies
  • response shapes and error models
  • enums, union types, pagination wrappers, etc.

Everything else—Kotlin controllers, TS API client, tests—is generated or checked against that contract. OpenAPI Generator, Swagger Codegen, and IDE integrations (IntelliJ) all support this style, including Kotlin server stubs and TypeScript clients. (GitHub)


Choosing the direction: spec‑first vs code‑first

You have two realistic options.

Code‑first Kotlin → OpenAPI → TypeScript

You implement the API in Kotlin (Spring Boot or Ktor), annotate controllers and models, and let tooling like springdoc or Swagger integrations emit OpenAPI. From there you generate the TS client and types.

Flow:

Kotlin controllers/models
          |
          v
    generated OpenAPI
          |
          v
 TS client + TS types

This works well when the backend is the “owner” of the contract and you’re wrapping existing services. Tutorials on Kotlin + Spring with OpenAPI follow this pattern. (baeldung.com)

Spec‑first OpenAPI → Kotlin + TypeScript

You write the OpenAPI spec by hand (or via a design tool), then generate:

  • Kotlin server stubs (kotlin‑spring, ktor, etc.)
  • TS client SDK / type definitions for the frontend

OpenAPI Generator and Swagger Codegen both support Kotlin and TS targets. (GitHub)

Flow:

    OpenAPI spec
        /  \
       v    v
Kotlin server  TS client + TS types

This shines when several clients (web, mobile, partners) share the same API and you want the contract independent from any one implementation.

In both cases, only the spec is allowed to define the wire format. No “secret” JSON fields invented in Kotlin or TS.


Shared types without a shared codebase

You should not share the same source files between backend and frontend, but you should share shapes.

On the backend you have:

  • Kotlin data classes (DTOs) generated or hand‑written to match OpenAPI schemas
  • domain models separate from DTOs

On the frontend you have:

  • TS interfaces/types generated from the same schemas
  • React components and state using those types

Conceptually:

OpenAPI schema: TxSummary
   ├─ Kotlin: data class TxSummaryDto(...)
   └─ TypeScript: type TxSummary = { ... }

Kotlin client generation articles and OpenAPI generator docs all emphasise this model: you generate model classes from the spec so you don’t hand‑sync fields. (openapi-generator.tech)

You still keep a clean boundary:

  • DTOs mirror the public API;
  • domain objects model your internal concepts;
  • mappers convert between the two on the backend.

The TS side never needs to know about the domain models; it just gets a stable, generated TxSummary.


Contract testing between frontend and backend

Even with a shared spec and generated clients, you can still drift if:

  • the backend doesn’t actually satisfy the spec, or
  • the frontend makes assumptions not captured in the spec.

This is where consumer‑driven contract testing (CDCT) comes in. Pact is the usual choice here: Pact JS for the TS side, Pact JVM for the Kotlin side. (GitHub)

The workflow looks like this:

TS consumer tests                Pact broker              Kotlin provider tests
------------------              ------------              ---------------------
1. Frontend tests define   ->   publishes pact      ->    4. Backend verifies
   expectations (paths,          files (contracts)        contracts against
   fields, examples)                                      running API

In practice:

  1. You write TS tests against a mock server provided by Pact, describing the requests the frontend will make and the responses it expects. That produces a contract file. (GitHub)

  2. You publish that contract to a broker or share it in CI.

  3. On the backend, Pact JVM reads the contract and replays the requests against your real Kotlin API, verifying that actual responses match the expectations. (codecentric AG)

If the backend accidentally changes a field type or leaves out a required property, the provider verification fails before you deploy.

This complements OpenAPI nicely:

  • OpenAPI: structure and types of the whole API
  • Pact: concrete expectations of specific consumers (your frontend, a bot, a partner)

Code generation strategies and CI wiring

Putting it all together, a typical pipeline for a Kotlin+TS blockchain service looks like this:

[ OpenAPI spec ]
       |
       +--> generate Kotlin server stubs / DTOs
       |
       +--> generate TS client SDK + types
       |
       +--> generate API docs

The OpenAPI Generator docs and examples show exactly that: one spec feeding multiple language targets. (GitHub)

In CI you then:

  1. Regenerate Kotlin and TS code from the spec.
  2. Compile Kotlin backend and TS frontend against the generated code.
  3. Run Pact consumer tests (TS) to refresh contracts. (docs.pact.io)
  4. Run Pact provider tests (Kotlin) against a running backend. (codecentric AG)

If:

  • generation fails → spec and templates disagree
  • compilation fails → app code disagrees with generated types
  • Pact fails → running backend disagrees with consumer expectations

You’ve caught a drift before it hits wallets, bots, or explorers.


Experience callouts

Production note – spec drift. On a multi‑chain explorer, we initially let the TS team maintain their own types by hand. Within a month, they had three “versions” of Transaction that didn’t match the Kotlin DTO anymore. Moving to spec‑first + generated TS models removed an entire class of bugs where fields were silently added or renamed.

Production note – Pact granularity. For a DEX backend we made a mistake early on: one giant Pact contract for the entire UI. It was noisy and fragile. Splitting contracts by feature (swaps, pools, portfolio) made failures meaningful: if the “swap” contract failed, we knew exactly which flow we’d broken.


Conclusion

End‑to‑end type safety across a TypeScript frontend and Kotlin backend isn’t about sharing code; it’s about sharing a contract and letting tools enforce it.

The pattern I keep coming back to is:

- OpenAPI as the single source of truth.
- Generated Kotlin DTOs/server stubs + generated TS clients/types.
- Consumer-driven contracts (Pact JS + Pact JVM) to pin down real usage.
- CI that regenerates, compiles, and verifies on every change.

Once that’s in place, adding new endpoints, chains, or transaction types becomes a controlled operation. The compiler and the contract tests tell you where to update behaviour, instead of leaving you to discover mismatches live on mainnet.

Completion scope and production contract

This completion review turns the earlier conceptual treatment into a release-oriented engineering contract. It treats End-to-End Type Safety: TypeScript Frontend to Kotlin Backend as a typed web, wallet, or blockchain API component, follows a user intent, API representation, wallet request, stream update, or transaction lifecycle event through validation and durable state, and separates normative requirements from implementation policy. The normative baseline is the published API or wallet specification, TypeScript model, and chain confirmation rules; deployment defaults, caching, retry limits, and operator thresholds are explicitly local policy. 7

The intended audience is experienced developers and architects. Readers should understand the surrounding chain or application model, typed data structures, persistence, and basic security engineering. The scope includes correctness, implementation boundaries, deterministic tests, failure recovery, security, performance, and observability. It does not claim that the educational companion is a drop-in replacement for a maintained protocol or cryptographic library. Production adoption requires an independent threat model, compatibility testing against the authoritative implementation, and operational ownership. 8

The mental model used throughout is deliberately strict: untrusted input crosses browser, wallet extension, transport, API, cache, and accessibility boundaries; a validator derives facts under the published API or wallet specification, TypeScript model, and chain confirmation rules; accepted transitions update server-derived data, wallet session, UI intent, and explicit transaction state; and observers consume committed facts, never optimistic intermediate mutations. A guarantee is stated only when it follows from those rules and assumptions. Heuristics such as fee selection, caching, peer scoring, timeouts, user messaging, or alert thresholds remain policy and may be tuned without redefining validity. 9

Reader contract and scope

For End-to-End Type Safety: TypeScript Frontend to Kotlin Backend, this review makes the exact user decision and the prerequisites needed to make it safely explicit. Start from one user intent, API representation, wallet request, stream update, or transaction lifecycle event and write down its origin, canonical representation, validation context, authority, and durable outcome. The typed web, wallet, or blockchain API component must not infer a stronger fact from transport success, cache presence, or an upstream acknowledgement. Its authoritative state is server-derived data, wallet session, UI intent, and explicit transaction state, and every projection must remain rebuildable or reconcilable from committed facts. This framing distinguishes a protocol guarantee from an operational convenience and gives reviewers a concrete place to challenge an assumption. 7

The principal failure to design against is an attractive example being mistaken for a complete production design. Address it before optimizing by defining a narrow ownership boundary, stable identities, bounded resource use, and a machine-readable outcome for every rejected transition. Record a scope statement, excluded concerns, and a reviewable acceptance criterion. A reviewer should be able to trace each accepted result to input bytes, rule or policy version, prior state, and commit identity without relying on prose logs. When the authority cannot be reached or context is incomplete, return an explicit unavailable or pending state; do not convert uncertainty into acceptance.

Precise vocabulary and authority

Treat precise vocabulary and authority as part of the executable design of End-to-End Type Safety: TypeScript Frontend to Kotlin Backend, not as documentation added after coding. The relevant operating envelope includes initial load, pagination, reconnect, chain switch, signing, pending confirmation, and partial API failure. For each mode, identify which state is authoritative, which work may be retried, what is bounded, and which observation proves progress. This is especially important across browser, wallet extension, transport, API, cache, and accessibility boundaries, where delivery and processing are different events and where local time or arrival order may not reflect authoritative order. 8

A useful review asks how the design behaves under stale closure, duplicate update, wallet rejection, chain mismatch, schema drift, reconnect storm, and misleading confirmation. The unsafe outcome is teams using the same word for incompatible states or guarantees. Prevent it with explicit preconditions and postconditions, and retain a glossary tied to the normative authority for every overloaded term as release evidence. Use stable codes rather than exception text, keep policy configuration versioned, and attach the accepted policy or rule version to durable results. An frontend or API-platform operator must be able to stop intake, drain or quarantine work, compare local state with authority, and resume without inventing a second side effect.

Trust assumptions

The implementation of End-to-End Type Safety: TypeScript Frontend to Kotlin Backend should expose which actors, clocks, stores, libraries, and upstream systems may fail or act maliciously through types and module boundaries. Parse external representations once, preserve the original identity when audit or replay needs it, and pass validated domain values inward. Mutations of server-derived data, wallet session, UI intent, and explicit transaction state belong behind one authoritative transition function or transaction boundary. Network clients, storage adapters, user interfaces, and telemetry exporters must not duplicate consensus or business rules. That separation keeps deterministic logic testable and prevents a library upgrade from silently redefining validity. 9

Assume that an implicit trusted component invalidating the claimed guarantee will eventually occur in a staging fault test or production incident. The control is not a catch-all retry: classify the outcome, decide whether the identical operation is safe to repeat, bound attempts and elapsed time, and surface terminal work for reconciliation. The minimum evidence is a trust-boundary diagram and an assumption register with owners. Keep addresses, wallet permissions, access tokens, KYC data, transaction intent, and browser storage out of ordinary logs, and prefer hashes, version identifiers, counts, and sanitized reason codes. Any emergency bypass must be narrow, time-limited, approved, and observable.

Architecture and ownership

Verification for End-to-End Type Safety: TypeScript Frontend to Kotlin Backend must demonstrate component responsibilities and the direction in which facts and commands move at several layers. Small tests cover deterministic transforms and boundary values; contract tests pin serialized forms; integration tests exercise real adapters; replay tests compare state roots or projections; and fault tests interrupt work at every commit boundary. The dataset must include normal history, malformed input, duplicates, reordering, maximum-size values, and changes of rule or schema version. A passing example is evidence about that environment and dataset, not a universal performance claim. 7

Make two components both believing they own the same transition a named negative test. The release packet should retain a context diagram, ownership table, and dependency rule, exact dependency and tool versions, the deterministic command, and its result. For performance work, report warm-up, repetitions, concurrency, percentiles, resource limits, and the point where backpressure begins. For security work, include abuse cases and independent review. A change is ready only when failures leave server-derived data, wallet session, UI intent, and explicit transaction state safe, recovery is rehearsed, and telemetry explains both user-visible outcome and operator action.

Canonical representation

For End-to-End Type Safety: TypeScript Frontend to Kotlin Backend, this review makes the byte-level or schema-level representation used for hashing, comparison, storage, and transport explicit. Start from one user intent, API representation, wallet request, stream update, or transaction lifecycle event and write down its origin, canonical representation, validation context, authority, and durable outcome. The typed web, wallet, or blockchain API component must not infer a stronger fact from transport success, cache presence, or an upstream acknowledgement. Its authoritative state is server-derived data, wallet session, UI intent, and explicit transaction state, and every projection must remain rebuildable or reconcilable from committed facts. This framing distinguishes a protocol guarantee from an operational convenience and gives reviewers a concrete place to challenge an assumption. 8

The principal failure to design against is semantically equal values producing different identifiers or verification outcomes. Address it before optimizing by defining a narrow ownership boundary, stable identities, bounded resource use, and a machine-readable outcome for every rejected transition. Record golden encodings, round-trip tests, and rejection of non-canonical forms. A reviewer should be able to trace each accepted result to input bytes, rule or policy version, prior state, and commit identity without relying on prose logs. When the authority cannot be reached or context is incomplete, return an explicit unavailable or pending state; do not convert uncertainty into acceptance.

State-machine model

Treat state-machine model as part of the executable design of End-to-End Type Safety: TypeScript Frontend to Kotlin Backend, not as documentation added after coding. The relevant operating envelope includes initial load, pagination, reconnect, chain switch, signing, pending confirmation, and partial API failure. For each mode, identify which state is authoritative, which work may be retried, what is bounded, and which observation proves progress. This is especially important across browser, wallet extension, transport, API, cache, and accessibility boundaries, where delivery and processing are different events and where local time or arrival order may not reflect authoritative order. 9

A useful review asks how the design behaves under stale closure, duplicate update, wallet rejection, chain mismatch, schema drift, reconnect storm, and misleading confirmation. The unsafe outcome is an impossible intermediate state becoming durable after interruption. Prevent it with explicit preconditions and postconditions, and retain a transition table exercised by positive, negative, and replay tests as release evidence. Use stable codes rather than exception text, keep policy configuration versioned, and attach the accepted policy or rule version to durable results. An frontend or API-platform operator must be able to stop intake, drain or quarantine work, compare local state with authority, and resume without inventing a second side effect.

Invariants

The implementation of End-to-End Type Safety: TypeScript Frontend to Kotlin Backend should expose properties that must hold before and after every accepted operation through types and module boundaries. Parse external representations once, preserve the original identity when audit or replay needs it, and pass validated domain values inward. Mutations of server-derived data, wallet session, UI intent, and explicit transaction state belong behind one authoritative transition function or transaction boundary. Network clients, storage adapters, user interfaces, and telemetry exporters must not duplicate consensus or business rules. That separation keeps deterministic logic testable and prevents a library upgrade from silently redefining validity. 7

Assume that local success concealing corruption in a related aggregate or index will eventually occur in a staging fault test or production incident. The control is not a catch-all retry: classify the outcome, decide whether the identical operation is safe to repeat, bound attempts and elapsed time, and surface terminal work for reconciliation. The minimum evidence is executable assertions at the narrowest authoritative boundary. Keep addresses, wallet permissions, access tokens, KYC data, transaction intent, and browser storage out of ordinary logs, and prefer hashes, version identifiers, counts, and sanitized reason codes. Any emergency bypass must be narrow, time-limited, approved, and observable.

Validation pipeline

Verification for End-to-End Type Safety: TypeScript Frontend to Kotlin Backend must demonstrate cheap structural checks, contextual checks, authoritative verification, and commit order at several layers. Small tests cover deterministic transforms and boundary values; contract tests pin serialized forms; integration tests exercise real adapters; replay tests compare state roots or projections; and fault tests interrupt work at every commit boundary. The dataset must include normal history, malformed input, duplicates, reordering, maximum-size values, and changes of rule or schema version. A passing example is evidence about that environment and dataset, not a universal performance claim. 8

Make expensive or stateful work running before malformed input is rejected a named negative test. The release packet should retain ordered validation stages with stable machine-readable rejection codes, exact dependency and tool versions, the deterministic command, and its result. For performance work, report warm-up, repetitions, concurrency, percentiles, resource limits, and the point where backpressure begins. For security work, include abuse cases and independent review. A change is ready only when failures leave server-derived data, wallet session, UI intent, and explicit transaction state safe, recovery is rehearsed, and telemetry explains both user-visible outcome and operator action.

Error semantics

For End-to-End Type Safety: TypeScript Frontend to Kotlin Backend, this review makes the distinction between invalid input, conflict, unavailable dependency, retryable interruption, and internal defect explicit. Start from one user intent, API representation, wallet request, stream update, or transaction lifecycle event and write down its origin, canonical representation, validation context, authority, and durable outcome. The typed web, wallet, or blockchain API component must not infer a stronger fact from transport success, cache presence, or an upstream acknowledgement. Its authoritative state is server-derived data, wallet session, UI intent, and explicit transaction state, and every projection must remain rebuildable or reconcilable from committed facts. This framing distinguishes a protocol guarantee from an operational convenience and gives reviewers a concrete place to challenge an assumption. 9

The principal failure to design against is blind retries amplifying a permanent failure or changing user intent. Address it before optimizing by defining a narrow ownership boundary, stable identities, bounded resource use, and a machine-readable outcome for every rejected transition. Record typed errors mapped consistently across logs, metrics, APIs, and queues. A reviewer should be able to trace each accepted result to input bytes, rule or policy version, prior state, and commit identity without relying on prose logs. When the authority cannot be reached or context is incomplete, return an explicit unavailable or pending state; do not convert uncertainty into acceptance.

Concurrency control

Treat concurrency control as part of the executable design of End-to-End Type Safety: TypeScript Frontend to Kotlin Backend, not as documentation added after coding. The relevant operating envelope includes initial load, pagination, reconnect, chain switch, signing, pending confirmation, and partial API failure. For each mode, identify which state is authoritative, which work may be retried, what is bounded, and which observation proves progress. This is especially important across browser, wallet extension, transport, API, cache, and accessibility boundaries, where delivery and processing are different events and where local time or arrival order may not reflect authoritative order. 7

A useful review asks how the design behaves under stale closure, duplicate update, wallet rejection, chain mismatch, schema drift, reconnect storm, and misleading confirmation. The unsafe outcome is a check-then-act race accepting two individually plausible operations. Prevent it with explicit preconditions and postconditions, and retain a linearization argument plus stress tests at the chosen contention boundary as release evidence. Use stable codes rather than exception text, keep policy configuration versioned, and attach the accepted policy or rule version to durable results. An frontend or API-platform operator must be able to stop intake, drain or quarantine work, compare local state with authority, and resume without inventing a second side effect.

Idempotency and replay

The implementation of End-to-End Type Safety: TypeScript Frontend to Kotlin Backend should expose how duplicate delivery, process restart, and historical backfill preserve the same result through types and module boundaries. Parse external representations once, preserve the original identity when audit or replay needs it, and pass validated domain values inward. Mutations of server-derived data, wallet session, UI intent, and explicit transaction state belong behind one authoritative transition function or transaction boundary. Network clients, storage adapters, user interfaces, and telemetry exporters must not duplicate consensus or business rules. That separation keeps deterministic logic testable and prevents a library upgrade from silently redefining validity. 8

Assume that at-least-once delivery creating a second side effect will eventually occur in a staging fault test or production incident. The control is not a catch-all retry: classify the outcome, decide whether the identical operation is safe to repeat, bound attempts and elapsed time, and surface terminal work for reconciliation. The minimum evidence is stable operation identities, deduplication state, and deterministic replay fixtures. Keep addresses, wallet permissions, access tokens, KYC data, transaction intent, and browser storage out of ordinary logs, and prefer hashes, version identifiers, counts, and sanitized reason codes. Any emergency bypass must be narrow, time-limited, approved, and observable.

Persistence and atomicity

Verification for End-to-End Type Safety: TypeScript Frontend to Kotlin Backend must demonstrate which facts commit together and how derived views catch up at several layers. Small tests cover deterministic transforms and boundary values; contract tests pin serialized forms; integration tests exercise real adapters; replay tests compare state roots or projections; and fault tests interrupt work at every commit boundary. The dataset must include normal history, malformed input, duplicates, reordering, maximum-size values, and changes of rule or schema version. A passing example is evidence about that environment and dataset, not a universal performance claim. 9

Make a crash exposing a cursor that claims work whose state was not committed a named negative test. The release packet should retain transaction boundaries, durable checkpoints, and reconciliation queries, exact dependency and tool versions, the deterministic command, and its result. For performance work, report warm-up, repetitions, concurrency, percentiles, resource limits, and the point where backpressure begins. For security work, include abuse cases and independent review. A change is ready only when failures leave server-derived data, wallet session, UI intent, and explicit transaction state safe, recovery is rehearsed, and telemetry explains both user-visible outcome and operator action.

API and schema contracts

For End-to-End Type Safety: TypeScript Frontend to Kotlin Backend, this review makes input limits, optionality, pagination, versioning, and compatibility behavior explicit. Start from one user intent, API representation, wallet request, stream update, or transaction lifecycle event and write down its origin, canonical representation, validation context, authority, and durable outcome. The typed web, wallet, or blockchain API component must not infer a stronger fact from transport success, cache presence, or an upstream acknowledgement. Its authoritative state is server-derived data, wallet session, UI intent, and explicit transaction state, and every projection must remain rebuildable or reconcilable from committed facts. This framing distinguishes a protocol guarantee from an operational convenience and gives reviewers a concrete place to challenge an assumption. 7

The principal failure to design against is a technically valid deployment silently changing a consumer-visible meaning. Address it before optimizing by defining a narrow ownership boundary, stable identities, bounded resource use, and a machine-readable outcome for every rejected transition. Record consumer fixtures, schema-diff checks, and explicit deprecation windows. A reviewer should be able to trace each accepted result to input bytes, rule or policy version, prior state, and commit identity without relying on prose logs. When the authority cannot be reached or context is incomplete, return an explicit unavailable or pending state; do not convert uncertainty into acceptance.

Security controls

Treat security controls as part of the executable design of End-to-End Type Safety: TypeScript Frontend to Kotlin Backend, not as documentation added after coding. The relevant operating envelope includes initial load, pagination, reconnect, chain switch, signing, pending confirmation, and partial API failure. For each mode, identify which state is authoritative, which work may be retried, what is bounded, and which observation proves progress. This is especially important across browser, wallet extension, transport, API, cache, and accessibility boundaries, where delivery and processing are different events and where local time or arrival order may not reflect authoritative order. 8

A useful review asks how the design behaves under stale closure, duplicate update, wallet rejection, chain mismatch, schema drift, reconnect storm, and misleading confirmation. The unsafe outcome is a correct core algorithm being exposed through an overpowered interface. Prevent it with explicit preconditions and postconditions, and retain abuse cases, permission tests, secret scans, and independently reviewed defaults as release evidence. Use stable codes rather than exception text, keep policy configuration versioned, and attach the accepted policy or rule version to durable results. An frontend or API-platform operator must be able to stop intake, drain or quarantine work, compare local state with authority, and resume without inventing a second side effect.

Adversarial analysis

The implementation of End-to-End Type Safety: TypeScript Frontend to Kotlin Backend should expose how a malicious party can shape inputs, timing, volume, ordering, and dependencies through types and module boundaries. Parse external representations once, preserve the original identity when audit or replay needs it, and pass validated domain values inward. Mutations of server-derived data, wallet session, UI intent, and explicit transaction state belong behind one authoritative transition function or transaction boundary. Network clients, storage adapters, user interfaces, and telemetry exporters must not duplicate consensus or business rules. That separation keeps deterministic logic testable and prevents a library upgrade from silently redefining validity. 9

Assume that tests covering accidents while ignoring deliberately pathological workloads will eventually occur in a staging fault test or production incident. The control is not a catch-all retry: classify the outcome, decide whether the identical operation is safe to repeat, bound attempts and elapsed time, and surface terminal work for reconciliation. The minimum evidence is a threat model linked to limits, monitoring, and incident playbooks. Keep addresses, wallet permissions, access tokens, KYC data, transaction intent, and browser storage out of ordinary logs, and prefer hashes, version identifiers, counts, and sanitized reason codes. Any emergency bypass must be narrow, time-limited, approved, and observable.

References