Main Our Publications
Shared TypeScript Contracts Between Angular and NestJS

Shared TypeScript Contracts Between Angular and NestJS

  • Angular
  • NestJS
  • TypeScript contracts
  • OpenAPI
  • API schema
  • code generation
  • backward compatibility
Shared TypeScript Contracts Between Angular and NestJS

Choose a shared package, OpenAPI generation, or schema-first contracts for Angular and NestJS without coupling the browser to backend entities or mistaking static types for runtime safety.

Shared TypeScript Contracts Between Angular and NestJS

Angular and NestJS speak the same programming language, but they do not share the same runtime or release moment. TypeScript checks source code before it runs; most type information is erased from the emitted JavaScript. HTTP turns values into bytes and back again. A browser tab opened yesterday can call an API deployed five minutes ago, and another consumer may be written in a different language altogether.

A reliable contract therefore describes what crosses the boundary: field names, wire formats, required and optional values, nullability, semantics, errors, authentication, pagination, and compatibility. It must also say how those claims are enforced at runtime and how change reaches independently deployed consumers. Merely importing the same interface into two projects proves only that both projects compiled against the same declaration.

Three patterns cover most Angular and NestJS systems: a deliberately small shared TypeScript package, a client generated from an OpenAPI document, or executable schemas that produce validators and static types. None is universally superior. Repository ownership, number of consumers, release independence, language diversity, trust boundary, and governance determine the best source of truth.

The useful question is not “How do we avoid defining a DTO twice?” It is “How do producers and consumers detect disagreement before users do?” The answer combines one authoritative contract, deterministic generation where needed, validation at the correct boundaries, compatibility tests, release policy, and production evidence about which contract versions are still in use.

Share the Wire Contract, Not the Implementation

The public contract should be smaller and more stable than either application’s internal model. Do not export ORM entities to the browser. Entities contain persistence columns, relations, lazy-loading behavior, audit fields, indexes, lifecycle hooks, and sensitive properties. A storage refactor should not accidentally rename a JSON field, expose an internal flag, or force Angular to understand a database relationship.

Keep backend domain models and Angular view models on their own sides of the boundary. NestJS maps validated request DTOs into commands or domain values and maps results into response DTOs. Angular maps transport responses into view state that may combine fields, parse dates, format money, or represent loading and permissions. This translation is useful insulation, not wasteful duplication.

Specify details that structural typing cannot infer safely: whether omission differs from null; whether a timestamp is an RFC 3339 string and in which timezone; whether money is an integer in minor units or a decimal string; whether identifiers are opaque; how enums evolve; how pagination cursors behave; which fields are writable; and which error shape is guaranteed. Document invariants that span fields, such as an end date following a start date.

Validate every untrusted request on the server. On the client, validate responses where the producer changes independently, data crosses a trust boundary, corruption would be costly, or offline persistence can outlive a release. Angular’s HttpClient generic is a compile-time assertion and does not inspect the returned JSON. The same contract discipline applies when a Backend for Frontend aggregates several services: aggregation does not remove the need for an explicit external shape.

Use a Shared Package When One Team Controls the Release Train

A shared package is effective when one organization controls the Angular and NestJS repositories, can publish packages reliably, and coordinates a reasonably short release train. It gives editors immediate type feedback, makes refactoring discoverable, and avoids a generation step. In a monorepo, project boundaries and affected-build tooling can make the feedback especially fast.

Keep the package deliberately boring. Include transport-oriented type aliases, request and response shapes, stable string values, error envelopes, and perhaps framework-neutral executable schemas. Exclude NestJS decorators and modules, ORM entities, repositories, domain services, Angular state, UI helpers, and environment-specific configuration. The browser should not pull server-only dependencies into its graph merely to learn the shape of an order.

Publish the package like an external dependency even inside a monorepo. Give it a public entry point, prohibit deep imports, pin or range versions intentionally, record changes, and test the compiled output. Avoid TypeScript path aliases that work in local builds but do not describe a deployable artifact. If runtime schemas are exported, confirm that the package format supports both server and browser bundlers without side effects.

A single commit updating the package, API, and web app proves that the newest versions agree. It does not prove rolling-deployment compatibility, nor does it protect users with an older browser bundle. Test the supported matrix: old client against new server, new client against old server when deployments can reverse, and stored offline data against the new reader. A shared package reduces transcription errors; release discipline provides safety.

// packages/order-contracts/src/public.ts
export const orderStatuses = ['pending', 'paid', 'cancelled'] as const;
export type OrderStatus = (typeof orderStatuses)[number];

export interface OrderSummaryContract {
  id: string;
  status: OrderStatus;
  totalMinor: number;
  currency: string;
  createdAt: string; // RFC 3339 on the wire
}

export interface OrdersPageContract {
  items: readonly OrderSummaryContract[];
  nextCursor?: string;
}

// Keep ORM entities and Angular view models elsewhere.

Use OpenAPI When Consumers Need an Independent Boundary

OpenAPI becomes the stronger boundary when teams release independently, clients live in separate repositories, non-TypeScript consumers exist, or an API is public. The specification is language-neutral, can be reviewed without the NestJS source tree, and supports documentation, client generation, mocking, linting, change detection, and governance from one portable artifact.

NestJS can generate an OpenAPI document from controllers and DTO classes, but TypeScript reflection has limits. Interfaces vanish at runtime, and complex generics, unions, mapped types, conditional shapes, and examples may require explicit decorators, registered extra models, or the Nest Swagger CLI plugin. The plugin reduces annotation work; runtime request validation still needs actual validators. Always inspect and test the generated document rather than assuming compilation made it complete.

Publish the specification as a versioned build artifact. Normalize it to avoid noisy diffs, lint naming and response conventions, compare it with the production baseline for incompatible changes, and generate clients with pinned tool versions. Fail continuous integration when generated output drifts. Never hand-edit generated files: wrap the generated transport in a small Angular adapter for authentication, retries, error mapping, domain translation, and tests.

Document failure as carefully as success. Include error bodies, status codes, authentication requirements, content types, headers, pagination, nullability, examples, and idempotency behavior. Exercise generated clients against a running API or a conformance suite. A beautiful specification that differs from production is worse than no specification because it creates justified-looking confidence in the wrong behavior.

npm --workspace api run export:openapi -- --output artifacts/openapi.json

# Compare with the deployed baseline, then regenerate deterministically.
npx openapi-diff baseline/openapi.json artifacts/openapi.json --fail-on-incompatible
npx openapi-typescript artifacts/openapi.json   --output apps/web/src/generated/api-schema.ts
git diff --exit-code -- apps/web/src/generated/api-schema.ts

Choose Schema-First When Runtime Proof Matters Most

Schema-first design starts with an executable schema and derives static types, runtime parsing, and sometimes OpenAPI or JSON Schema from it. It fits systems where both Angular and NestJS must validate unknown data, where events or persisted payloads outlive one deployment, or where framework-neutral governance matters more than decorator-driven documentation.

Choose the schema library and conversion path from the required contract features, not popularity. Test how it represents input coercion, output transformation, discriminated unions, recursion, dates, brands, defaults, refinements, unknown keys, and distinct input and output types. A conversion that silently weakens a refinement or changes requiredness may produce a valid-looking OpenAPI document with different semantics.

Place validation deliberately. NestJS should parse external input before it reaches domain logic and should serialize an intentional response. Angular can parse critical or independently changing responses before updating state. Background workers should validate queue messages at consumption, not trust the producer’s compile. Decide whether unknown fields are rejected, stripped, or preserved, and return errors that identify the failing path without leaking sensitive values.

A schema does not replace authorization, database constraints, or domain rules. “This is a valid UUID” does not mean the caller may access that tenant. A syntactically valid amount may exceed credit policy. Keep transport validation close to the boundary and business invariants in the domain. Maintain one authoritative definition and generate secondary artifacts; parallel handwritten types, validators, and documentation inevitably drift.

Make Compatibility a Release Discipline

Compatibility is a property of two versions in conversation, not a label attached to one release. Adding an optional response field is usually compatible when consumers tolerate unknown fields. Removing or renaming a field, changing its meaning or format, making a response nullable, adding a required request field, narrowing accepted input, or changing error semantics is usually breaking. Even an enum addition can break a client that assumes its switch is exhaustive at runtime.

Prefer tolerant readers and conservative writers. Consumers should ignore unknown response fields and handle unknown enum values safely when the business permits it. Producers should reject ambiguous input rather than guessing, preserve documented defaults, and avoid reusing names or enum values with new meanings. Define whether absence, null, empty text, zero, and an empty collection are distinct.

Use expand-and-contract for change. First deploy a server that accepts old and new input and can produce a response older consumers understand. Then release consumers, observe adoption and failures, migrate stored data if necessary, and remove the old form only after the support window closes. Version a package semantically and introduce a new HTTP version only for genuine, intentionally supported breaks; version proliferation is not a substitute for migration.

Automate evidence in continuous integration: schema linting, generated-artifact drift checks, breaking-change comparison, serialization examples, provider tests, consumer expectations, and end-to-end tests using released versions. In production, record contract or client version with bounded labels, measure deprecated-field use, and define an owner and removal date. The safest contract strategy is the one a team can operate consistently, not the one with the most sophisticated generator.

Contract Strategy Checklist

  • Choose from operating reality: select a shared package, OpenAPI, or schema-first source from repository ownership, consumer languages, release independence, runtime validation, and the team’s ability to govern generation.
  • Protect the boundary: keep ORM entities, domain behavior, server decorators, persistence details, Angular state, and UI helpers outside the wire contract; translate explicitly on both sides.
  • Define semantics: specify dates, timezones, money, identifiers, omission versus null, enum evolution, unknown fields, pagination, authentication, idempotency, errors, and cross-field invariants.
  • Automate proof: pin generators, produce secondary artifacts deterministically, fail CI on drift or incompatible changes, and test serialization plus runtime validation with valid and invalid examples.
  • Release compatibly: test old and new client-server combinations, use expand-and-contract, observe deprecated behavior in production, and remove an old shape only after its support window and measured use have ended.
Stop Angular and NestJS Contracts from Drifting

Share the repositories, DTOs, OpenAPI documents, generated clients, release cadence, runtime validators, and recent compatibility failures. We will identify one source of truth and define an incremental path with measurable safeguards.

We use cookies to ensure the security and proper functioning of our website. With your consent, we also use non-essential cookies for analytics and advertising purposes. You can accept or reject the use of non-essential cookies. You can change your preferences at any time. Learn more in our Cookie Policy.