Skip to main content

Command Palette

Search for a command to run...

Contract Testing Without Pact: Using Shared Zod Schemas as a Source of Truth

Updated
5 min read

TL;DR

Contract testing prevents frontend and backend drift. Tools like Pact automate this but introduce additional complexity and duplicate definitions. Shared Zod schemas offer a simpler alternative in TypeScript environments by acting as a single source of truth for runtime validation and type safety. The tradeoff is less automation, but fewer moving parts.

The key idea is simple: start with the smallest solution that enforces contract discipline, and add tooling only when it becomes necessary.


The Problem Every Microservice Architecture Eventually Hits

Splitting an application into separate frontend and backend repositories brings clear advantages. Teams can deploy independently, codebases remain smaller and easier to manage, and responsibilities are well separated.

However, this separation introduces a subtle but common failure mode. The frontend and backend can drift apart without immediate visibility.

A backend field might be renamed. A frontend might assume a value is always present. An enum might gain a new value on one side but not the other.

Everything can still pass locally. Unit tests pass. Integration tests pass. But once deployed, real users encounter broken experiences.

This is exactly the class of problem that contract testing is meant to solve.


Two Common Approaches

One common solution is Pact, a consumer-driven contract testing framework. In this model, the consumer defines its expectations, a contract file is generated, and the provider verifies that contract in CI. Pact provides built-in automation, contract versioning through a broker, and works across multiple programming languages.

Another approach is to use shared Zod schemas. In this model, a shared package defines request and response schemas that are imported by both frontend and backend. Tests on both sides validate real payloads against these schemas. This creates a single shared definition of the contract.


Why a Schema-Based Approach Can Be Enough

In many TypeScript systems, schema libraries like Zod are already used for runtime validation, type inference, and defining API shapes.

Because of this, contract testing can often be achieved by extending the role of these schemas rather than introducing a new tool.

The main advantage is that the same definition serves multiple purposes. It validates data at runtime, provides compile-time types, and defines the contract between systems.


The Core Issue: Duplication

When evaluating contract testing solutions, one question matters more than it seems at first:

How many systems are describing the same data?

With Pact, contract definitions live separately from application schemas. With shared schemas, a single definition is reused everywhere.

Maintaining multiple representations of the same contract increases the likelihood of inconsistencies, drift between tools, and additional maintenance overhead.

Avoiding duplication is often more valuable than adding automation.


A Simple Layered Model

A schema-based contract testing approach can be structured in three layers.

First, each service defines its own schemas for input validation, output shaping, and type definitions. These schemas power the actual API implementation.

Second, a separate shared package defines integration schemas. These represent the external contract, describing what consumers expect and what providers guarantee. These schemas focus on the integration boundary rather than internal implementation details.

Third, both frontend and backend validate against these shared schemas in tests. Backend tests validate API responses, while frontend tests validate the data they receive.

This ensures that any mismatch is caught early. If the backend changes something breaking the contract, its tests fail. If the frontend expects something new, the shared schema must be updated, and mismatches become immediately visible.


Trade-offs: Zod vs Pact

Pact provides built-in automation, contract verification in CI, and strong support for multi-language environments. However, it introduces additional tooling, separate contract definitions, and operational overhead.

Shared Zod schemas provide runtime validation, compile-time types, and a single source of truth with minimal additional tooling. The tradeoff is that verification is test-driven rather than automatically enforced by a framework.

The choice is less about correctness and more about where to place complexity.


When Shared Schemas Work Well

A schema-based approach is a strong fit when both frontend and backend use TypeScript and a schema library like Zod is already part of the stack.

It also works well when the number of consumers is small, teams can coordinate changes, and reducing tooling complexity is a priority.


When Pact Is a Better Fit

Pact becomes more valuable when systems span multiple programming languages or when there are many independent consumer teams that do not coordinate closely.

It is also useful when centralized tracking of compatibility across environments is needed, or when teams prefer built-in automation over maintaining their own test-based validation.


Key Lessons

Start with the tools already available. Before adding a new framework, it is worth asking what problem it solves that cannot already be addressed.

Avoid duplicate sources of truth. Two systems describing the same contract create more problems than they solve.

Separate definition from validation. Service schemas should define implementation, while integration schemas define contracts. This separation improves reliability and avoids circular validation.

Keep decisions flexible. What works for a small system today may not work as it scales. Architectural choices should evolve over time.


The Takeaway

Contract testing is not about choosing a specific tool. It is about enforcing agreement between systems and catching mismatches before they reach users.

This can be achieved with dedicated frameworks like Pact or with shared schemas and structured testing.

In many TypeScript environments, shared schemas provide a practical balance. They offer a single source of truth, runtime validation, type safety, and minimal additional tooling.


Final Thought

Instead of asking whether to use Pact or Zod, a better question is:

What is the smallest solution that reliably prevents contract drift?

Start there, and add complexity only when it becomes necessary.