Designing Systems Before Coding Them
Many teams still generate API contracts after the implementation. Designing the interface first gives backend, frontend, QA, and service consumers one artifact to review, validate, and build against before integration problems become expensive.
Start with the contract
Many teams still treat the API contract as a byproduct. They write handlers, add DTOs and validation, then expose documentation from the running application. Frontend, QA, and other services begin integrating only to find that nullable fields, enums, edge cases, and error formats have been interpreted differently. Client code has already spread across repositories by then.
At small scale, this is survivable. At platform scale, it becomes delivery drag.
Spec-first development moves those decisions ahead of implementation. Once the contract is a source artifact, the design is no longer hidden inside controller code. Teams can review it, generate from it, and reuse it across system boundaries.
A contract is an input to delivery
OpenAPI can do more than populate Swagger UI. AsyncAPI can do more than catalogue topics, and JSON Schema can do more than validate a payload.
Taken together, they describe the boundary of a system in a form that both humans and tooling can understand.
That boundary includes far more than field names and primitive types. It records which payload shapes are allowed, which fields are required, and which format or structural constraints apply. It also covers errors, authentication, roles and scopes, compatibility rules, event payloads, and model semantics shared by backend and frontend.
Defining the boundary up front lets teams work from an agreed design instead of reconstructing it from implementation details.
What code-first usually looks like in real systems
Code-first approaches feel fast: write the endpoint, decorate it, and generate documentation from framework metadata. Some tools can also infer schemas from DTOs or TypeScript types. The convenience is useful, especially in a small service, but the design is only reviewable after it has become code.
In simple services, it can work well enough. In larger systems, documentation quality becomes tied to framework conventions, while the design is reviewed only after it already exists in code. Generated contracts tend to reflect transport structure rather than design intent. Over time, backend and frontend models drift, schema reuse becomes inconsistent, validation is duplicated, and client libraries end up with partial typing or no runtime guards.
Code-first is not always the wrong choice. It does, however, make implementation the place where interface decisions can happen by accident. Spec-first puts those decisions in review while they are still cheap to change.
Why JSON Schema is more powerful than most teams realize
Many teams already use JSON Schema indirectly. It appears inside OpenAPI, validation tooling, form generation, and configuration checks, yet it is often treated as plumbing rather than a shared description of system boundaries.
JSON Schema is one of the most effective boundary-definition tools available for modern backend platforms. Its machine-readable model of structures and constraints can feed API specifications, runtime validation, generated types, frontend forms, mocks, test fixtures, contract diffing, and shared platform libraries.
Its value comes from giving different parts of the delivery chain one representation they can use consistently, not from adding another schema language.
When teams say they want a “single source of truth,” this is one of the few places where that phrase can actually mean something concrete.
Spec-first at platform level
We saw the largest benefit when building the Fizz backend platform, where spec-first became a platform capability rather than a preference left to each service.
The OpenAPI document lives as static YAML in the service source. We generate handlers, types, and validators from it, then use AJV on both backend and frontend. CI keeps the HTTP clients in sync for backend consumers, frontend applications, and tests. Those tests can run against mocks without hardcoding URLs, methods, or payload assumptions.
The contract drives client generation, so consumers do not reconstruct service calls by hand. Backend validation and frontend assumptions use the same schema vocabulary, while generated clients keep tests from duplicating request details.
Developers reported better DX because they no longer had to memorise interface details. The time saved on each call is small; repeated across services and repositories, it becomes meaningful.
Static OpenAPI as a source artifact, not an exported artifact
One subtle but important implementation detail is where the contract lives.
In many code-first setups, the API document is something generated by a running application. That creates a dependency chain where the contract is derived from code and often only exists reliably after compilation or bootstrapping.
A static OpenAPI YAML checked into the service source reverses that dependency. The specification exists before the service runs, can be reviewed in a pull request, and can be linted or validated independently. It can drive code generation, breaking-change checks, documentation publishing, and test tooling before implementation is complete. The contract becomes a development input that the platform can govern.
Visual: implementation-first vs spec-first
flowchart TD
A[Write handlers and DTOs] --> B[Generate docs from framework metadata]
B --> C[Consumers interpret behavior]
C --> D[Integration issues found late]
D --> E[Patch code, docs, and clients]
F[Design OpenAPI / AsyncAPI first] --> G[Review schemas and behavior]
G --> H[Generate types, validators, handlers, clients]
H --> I[Implement against agreed contract]
I --> J[Integrate earlier with less drift]
Generated clients remove duplicated interface code
Generated clients save time, but their more durable benefit is consistency.
Without generated clients, consumers replicate the interface by hand. Paths and query parameters are assembled locally, HTTP methods are recalled from memory, and auth headers are wired differently in each client. Request and response typing is often partial, error handling varies, tests hardcode endpoint details, and topic names or payload structures drift in evented systems.
Each repetition creates another place for the implementation and its consumers to diverge.
When a CI pipeline ensures the clients are generated from the current contract, you substantially reduce that category of error. Consumers stop depending on memory and conventions. They depend on artifacts derived directly from the contract.
In our case, that also improved testing. Generated clients can be used in end-to-mock tests, which means tests are written against the same contract-driven surface that production consumers use. No duplicated URLs, no hand-rolled fetch wrappers, no magic strings for methods or route shapes.
The tests become less brittle because they stop carrying a second, handwritten version of the interface.
Types stop at runtime
TypeScript helps developers reason about expected shapes during development, but it cannot validate external input at runtime. Malformed payloads can still arrive from other services, older clients, partially rolled-out consumers, or third-party integrations.
If your handlers, clients, and forms are all rooted in the same schema family, runtime validation becomes consistent across system boundaries.
This matters on the backend, where you need to validate inbound requests and sometimes outbound contracts. It matters on the frontend too, where you need to validate user input and ensure the payload you are about to send still conforms to the API contract.
Using AJV on both sides helps close the gap between static intent and runtime reality.
The type system describes what your own code expects. The validator checks what actually crossed the boundary. You need both.
The same schemas can support the frontend
Spec-first discussions often stay backend-centric, even though frontend and admin tooling can reuse the same schemas.
Once your APIs are described with JSON Schema-backed contracts and the same schemas are accessible to frontend applications, you can start doing much more than generating clients.
You can use libraries like JSON Forms to simplify admin interface development significantly.
Generating a whole customer-facing frontend from a schema is usually too crude. For internal tools, admin backoffices, operations screens, configuration editors, and workflow forms, however, schema-driven UI can remove a large amount of repetitive work.
The pattern is especially effective when you separate concerns cleanly:
- JSON Schema describes the data contract and validation semantics
- UI Schema describes layout and presentation concerns
- AJV validates form data against the same schema model used by the API contract
This creates a strong end-to-end alignment:
- The backend contract defines what valid payloads look like.
- The frontend form can be generated or strongly assisted from the same schema family.
- The UI schema can control rendering, grouping, widgets, and layout.
- The submitted form payload can be validated before sending.
- The backend validates the same structure again on receipt.
This avoids reimplementing field definitions, validation rules, and structural assumptions in every layer. Admin surfaces need less repetitive UI code and stay closer to the API contract.
JSON Forms and schema-driven admin surfaces
Internal platforms often accumulate a long tail of operational forms: product attribute editors, pricing configuration, integration setup, rules and policy editors, merchant onboarding, support tools, and feature configuration panels.
These interfaces are important but rarely differentiate the product. They need to be correct, maintainable, and easy to change; most fields do not need bespoke UX.
With JSON Forms or similar tooling, you can define the shape and validation in JSON Schema, use UI schema for presentation and component selection, and still preserve strict compatibility with the backend contract.
The same model removes duplicated field definitions and keeps validation messages consistent. New admin surfaces are easier to produce, schema changes cost less to maintain, submitted data stays aligned with the API contract, and developers can learn the internal tooling more quickly.
Controlled reuse of the contract model is the goal. Blindly generating every screen is not.
Visual: schema-driven flow from API to admin UI
flowchart TD
A[OpenAPI + JSON Schema] --> B[Generated backend handlers and validators]
A --> C[Generated typed clients]
A --> D[Frontend form model]
D --> E[UI Schema controls layout and widgets]
E --> F[JSON Forms renders admin UI]
F --> G[AJV validates on frontend]
G --> H[Request sent via generated client]
H --> I[AJV validates again on backend]
Event contracts expose otherwise invisible coupling
Spec-first becomes even more important when the system is not purely synchronous.
HTTP at least makes interfaces visible. Endpoints have paths, methods, and status codes. Messaging systems are often much less self-describing once they start growing. Topics multiply. Message payloads evolve informally. Similar events appear with slightly different semantics. Consumers rely on undocumented assumptions.
AsyncAPI and disciplined schema reuse give those dependencies a visible form.
In event-driven systems, ambiguity is more dangerous because failures are often delayed and distributed. A malformed assumption may not fail loudly. It may quietly corrupt downstream behavior or break an integration in ways that are expensive to trace.
Explicit event contracts record message payloads and ownership boundaries, along with the versioning approach, correlation identifiers, compatibility rules, examples, and semantic intent.
As with HTTP APIs, a source-controlled event contract can participate in validation, review, generation, and governance. Without it, event-driven systems accumulate coupling that is hard to see until a consumer breaks.
Put security intent next to the interface
In too many systems, authorization logic is added after the interface shape is already decided. Endpoints get protected in code, roles are implied rather than modeled, and policy expectations are distributed across framework annotations, middleware, and service-specific conventions.
When scopes, auth schemes, and protected operations are represented in the contract, several things get easier:
- reviewers can inspect security intent earlier
- generated artifacts can understand auth requirements consistently
- consumer teams know what credentials or scopes are needed
- gaps become more visible during design review instead of after rollout
This does not replace sound authorization architecture. It does put security intent next to the interface, where reviewers and consumers can see it.
A stable contract lets teams work in parallel
Once the contract is stable enough:
- backend can implement handlers
- frontend can consume generated clients
- QA can derive test scenarios and fixtures
- mocks can be produced from the spec
- integration tests can start earlier
- consumer services can develop against the contract without waiting for a fully deployed provider
The shared artifact narrows uncertainty enough for concurrent work. Backend, frontend, QA, and consuming services no longer have to wait for a deployed provider before making progress.
CI keeps the contract authoritative
Once CI ensures that the specification is valid and generated artifacts stay current, the workflow becomes much harder to bypass accidentally.
Typical checks in a mature setup include:
- OpenAPI or AsyncAPI validation
- schema linting
- breaking-change detection
- generation of handlers, types, validators, and clients
- verification that generated code is committed or published correctly
- test execution against generated clients and mocks
These checks make the contract authoritative instead of leaving contract-first as a team preference that depends on memory.
Visual: spec-first platform workflow
flowchart TD
A[Update OpenAPI / AsyncAPI spec] --> B[PR review]
B --> C[Validate schema and examples]
C --> D[Check breaking changes]
D --> E[Generate types, validators, handlers, clients]
E --> F[Run tests with generated clients and mocks]
F --> G[Publish service and contract artifacts]
Where spec-first can go wrong
Weak schemas, poor generated artifacts, or a specification treated as bureaucracy can make the process heavy without adding much value.
It usually fails when:
- specs are written but not authoritative
- validation exists only on one side of the boundary
- examples are missing
- generated code quality is too low
- versioning and compatibility rules are unclear
- teams model trivial details too aggressively
- nobody owns the contract lifecycle
Use spec-first where contracts matter. Keep schemas readable, generate only the artifacts that save work or prevent drift, enforce the workflow in CI, and treat contract reviews as design reviews.
The payoff grows with the number of consumers
Early in a product's life, almost any interface approach can work because there are few consumers and the feedback loop is tight. The cost profile changes as services, teams, environments, and compatibility requirements multiply.
With more services, frontend surfaces, teams, and environments come greater compatibility pressure, stronger governance requirements, more operational tooling, and more dependence on reliable automation. Interfaces are now part of the platform rather than a local implementation detail. Spec-first pays off because the contract can execute across the delivery pipeline, not merely document it.
Make the contract do work
Spec-first makes contracts explicit early enough for tooling, tests, validators, clients, and teams to rely on the same source model.
JSON Schema can connect API design, runtime safety, client generation, admin UI generation, and platform governance. Treating it only as validation plumbing leaves much of that value unused.
On the Fizz backend platform, that means static OpenAPI in the service source, generated handlers, types and validators, and AJV on both backend and frontend. CI keeps generated clients current for services, frontend code, and tests; JSON Forms reuses the same schemas for admin UI work.
The result is better developer experience, less duplicated work, fewer integration surprises, and a platform that is easier to evolve safely.
If your team already uses OpenAPI, AsyncAPI, or JSON Schema, the remaining step is to let those contracts shape how the system is built instead of generating them after the fact.