Achieving polyglot schema isomorphism without losing control of your architecture.
WebDevelopment #Architecture #OpenAPI #Angular #Symfony #PHP #Java #DotNet
In modern web architecture, decoupling front-end and back-end teams is standard practice. However, this separation often introduces a pervasive friction point: data serialization, type alignment, and runtime validation
validation. When your front-end runs on Angular and your back-end relies on Non Javascript languages (Symfony PHP, Java Spring, DotNet, Python), achieving type isomorphism usually meant relying on manual synchronization, expensive runtime exceptions, or finally, forcing NodeJS and TypeScript onto the server.
We eliminated this friction entirely. By implementing a strict Contract-Driven Development workflow using OpenAPI 3.1 and JsonSchema, we achieved cross-language schema consistency. Our architecture leverages a dedicated Backend-for-Frontend (BFF) layout interacting with business microservices, ensuring that the Angular client only interacts with a fully synchronized, typed contract. Here is how we built it, the pitfalls we navigated, and how you can apply these architectural patterns.
We will consider using PHP Symfony for our BFF in this article. Don’t really take care at this because this is just for sample, and there is no code ;-)
The Single Source of Truth Stack
Instead of manually writing TypeScript interfaces and PHP Data Transfer Objects (DTOs), we treat the API specification as a standalone codebase. The architecture relies on an isolated repository structured around
three primary pillars:
OpenAPI 3.1 & JsonSchema: OpenAPI governs our HTTP transport layer (routes, parameters, HTTP
status codes), while highly granular JsonSchemas handle payloads. Every schema is strictly named — there are zero anonymous schemas in our repository.
Dual Packaging: To make this ecosystem immediately accessible to both languages, the single contract repository is dual-packaged. It includes a package.json for NPM consumption (Angular side) and a
composer.json for Packagist/Composer consumption (Symfony side).
Standardized Errors: We adopted the JSON:API error specification. This standardizes error objects natively, providing uniform pointers to form fields, error codes, and human-readable context across every
single endpoint.
Embedding Business Rules Directly Into the Schema
One of the greatest paradigm shifts in our design was pushing validation constraints upstream into the contract itself, rather than keeping them trapped inside PHP or TypeScript logic. We moved beyond simple type checking to embed rich business rules natively inside the JsonSchemas:
String constraints (e.g., minimum and maximum length bounds for structured fields).
Value initialization via explicit defaults.
Complex temporal and relational constraints (e.g., establishing a minimum age requirement for specific entities, or anchoring a minimum date dynamic for uploaded documentation).
Crucially, we leveraged advanced JsonSchema conditional structures like if/then/else . This allows us to represent deep business dependencies within the contract — such as marking specific identification properties
as required if and only if the payload indicates a legal adult.
Write on Medium
The Automated Toolchain: Linting, Bundling, and CodeGen
To scale a multi-file schema without breaking downstream services, we engineered an automated pipeline
using industry-standard open-source tooling:
Linting with Stoplight Spectral
To avoid style drift and structural anomalies, Spectral acts as our continuous integration gatekeeper. It
enforces strict compliance to OpenAPI 3.1 formatting rules and custom team conventions before any contract
version can be published.
Bundling with Redocly
Maintaining a monolithic OpenAPI file is a developer experience nightmare. We split our files cleanly by
domain boundaries, functional entities, and dedicated routes. At build time, the Redocly CLI flattens and
bundles these distributed files into a single, comprehensive OpenAPI delivery artifact.
Sandboxing with Elements
Rather than using bloated runtime GUIs, we integrated Stoplight Elements. It provides an interactive, beautiful
API sandbox for developers to read documentation and execute live mock queries directly from the generated
spec.
Runtime Guardrails on frontend: AJV
To safeguard our systems against data corruption at runtime, we implemented AJV (Another JSON Validator). On the front-end, we strategically isolate runtime verification to our service layer test suites to ensure the user interface is never penalized with computing overhead. On the back-end, AJV controls both
inputs and outputs strictly on live requests to prevent database contamination or internal structural leakage.
Crucial Dev Tip: Pin your tools. OpenAPI Code Generators evolve fast. Always freeze your configuration inside openapitools.json to ensure every developer and CI/CD runner is executing the exact same binary compiler. Subtle micro-version shifts can drastically alter generated models.
Hard-Earned Architectural Lessons & Pitfalls
The Hidden Complexities of allOf and Conditionals
Combining structural inheritance via allOf with conditional logic ( if/then statements) can easily break popular code generation engines. If your sub-schemas allow additionalProperties , or if you nest intricate object structures inside conditional blocks, code generators like OpenApiGenerator often fail or produce flawed types. To circumvent this, explicitly favor explicit definitions over heavy composition, and break
complex embedded objects out into their own schemas via explicit $ref links. This ensures predictable generation paths.
Protect Your Domain: The Hexagonal Adapter Pattern
Never let code-generated entities flood your application domain. If your views, components, and backend business logic directly bind to models spit out by a CLI tool, you are locking yourself into a severe dependency
trap. A minor tool upgrade can force a sweeping refactor across your entire codebase.
The Strategy: Implement strict structural Adapters and Factories. Your application should only ever interact with native, domain-owned models. The generated HTTP clients and objects should remain encapsulated
within data layers, where specialized Factories immediately map those remote objects into your internal domain space upon arrival.
Conclusion
Contract-Driven Development shifts integration issues from a high-stakes production incident to a deterministic, compile-time validation check. By transforming our API contract into an independent, versioned,
dual-packaged module, Angular and Symfony communicate across a zero-friction border — retaining their native languages while behaving like a perfectly unified, isomorphic system.
Top comments (0)