Engineering

Bridging the Gap: Benefits of Contract Testing in Frontend-Backend Integration

In this blog, we’ll explore a recurring challenge our team faced while collaborating on a project that involved both backend and frontend teams. Ensuring synchronization between both sides is crucial, yet it can be complicated. Changes in the backend frequently caused unexpected issues on the frontend, underscoring the need for effective coordination and early detection of discrepancies in the development cycle.

Without an automated system to check for changes in API responses or data formats, the teams had to rely on manual checks after each deployment. This process was both time-consuming and error-prone, with issues typically surfacing only when the app began exhibiting unexpected behavior—like when unrecognized data structures triggered HTTP 400 errors.

That’s why we were looking for a solution that could actively validate compatibility between frontend and backend, ensuring their automatic alignment – and we found it in contract testing.

The Challenges of Backend-Frontend Dependency

Because of this dependency, a single change can often affect critical parts of the application. 

For us, a minor backend update once altered a field in the user profile API. This seemingly small change created a cascade of bugs across essential frontend components, leading to the incorrect display of user data. Requests that had previously functioned without issue now began failing, which resulted in extensive debugging sessions for the frontend team, eating into valuable development time.

Resolving these bugs required ongoing communication between the frontend and backend teams. A frontend developer typically had to reach out to the backend developer responsible for the feature, and together, they would trace the changes to pinpoint the exact source of the problem. This back-and-forth dialogue consumes significant time and resources, adversely affecting overall productivity and causing delays in feature development.

This is what led us to realize the need for a more effective way to keep backend and frontend systems aligned automatically, reducing unexpected issues and boosting team efficiency.

Exploring Solutions – TypeScript Models

To tackle the challenge of keeping API payloads consistent between the two teams, our initial exploration involved generating TypeScript models directly from the Swagger API schema. This approach aimed to ensure that the frontend remained aligned with backend expectations.

At first, this method seemed promising. Generating models from the backend’s API schema could eliminate the need for manually coding data structures on the frontend and help maintain synchronization with backend updates. It also offered a centralized API reference, making it easier for frontend developers to understand the data.

However, this method had several limitations for both frontend and backend.

Frontend limitations

  • Too reactive

Although the TypeScript models accurately reflected the API schema at one point, they did not accommodate ongoing backend changes. Whenever the backend team updated the API, the models needed to be regenerated and re-integrated into the frontend codebase. This added step required extra effort and coordination, and any delay in updating the models could lead to inconsistencies and potential application failures.

  • Lacks adaptability to frontend

A major challenge was that the generated code did not align with our frontend’s structure or naming conventions. Following a backend-centric schema often led to verbose implementations that were inflexible for frontend needs, affecting readability. Even minor UI adjustments sometimes required manual changes or complete regeneration of the models, significantly slowing our ability to customize them for frontend use cases.While generating TypeScript models from the Swagger API schema provided some initial alignment, it failed to fully address the ongoing need forconsistency. As our project scaled, we recognized that this approach was too reactive and didn’t deliver the seamless integration we sought.

Backend limitations

From the backend perspective, relying on generated Swagger schemas presented significant challenges.

Swagger is a valuable tool for documenting APIs, but it has limitations:

  • The manual process of checking for new changes and replacing schemas with each modification is inefficient.
  • This manual oversight does not guarantee that breaking changes won’t negatively impact production, potentially leading to bugs and disruptions.

Communication of new changes to the frontend team is often inconsistent:

  • The lack of automation or integration into our CI/CD pipeline creates gaps in ensuring consistency and reliability.
  • Without a streamlined process, the risk of discrepancies increases.

The need for a more automated solution became apparent:

  • A solution that could validate changes against existing contracts is essential for ensuring alignment between teams.

This realization led us to seek a more robust solution:

  • We needed a strategy that actively validates compatibility between the frontend and backend, rather than just replicating the backend’s schema periodically.
  • Our exploration of contract testing emerged as a proactive strategy for managing alignment between the two sides.

What is Contract Testing

What is Contract Testing_Visualization

At its core, contract testing is a method for defining and verifying “contracts” between API consumers — such as frontend applications or other services using our backend API — and providers, which are the backend API services themselves. It ensures that the API’s actual responses align with the expectations of its consumers.

Provider-Driven vs Consumer-Driven Contract Testing

There are two primary approaches to contract testing: provider-driven and consumer-driven.

Provider-Driven ApproachConsumer-Driven Approach
The API provider defines the contract, which consumers use to validate API expectations.The consumer defines the contract, and the provider ensures the API meets this specification.
Preferred when there are many consumers.Effective when there are fewer consumers, as the consumer defines its specific data needs.
Useful when consumers are unclear about expected data, ensuring a consistent standard.Allows consumers to specify precise data requirements, leading to tailored data responses.
Consumers have limited input on contract details.Puts the burden on the provider to adapt to each consumer’s contract, which can add complexity.

Why We Chose Consumer-Driven Contract Testing

Embracing consumer-driven contract testing enabled us to address one of our most significant challenges: the need for automated real-time checks that would catch any API mismatches before they reached production. Rather than expecting the frontend to reactively adapt to backend changes, contract testing empowered the frontend to define its data requirements upfront.

Consequently, the backend team became responsible for ensuring that its responses adhered to the frontend’s contract. This shift in responsibility facilitated a more independent workflow for both teams, allowing them to work autonomously while remaining in sync.

Using Pact to Ensure API Consistency

While contract testing is language-agnostic, our backend was written in Java. After carefully evaluating our options, we chose an existing tool called Pact. because it provided the comprehensive features we required for consumer-driven contract testing. Pact supports both the consumer and provider sides of testing, making it an excellent fit for our project needs.

How It Works

On the consumer side, we define tests that specify the expected contract in terms of payload. We can establish requirements for each payload field, determining whether we care only about data types or specific values.

When these tests are executed, a contract file is generated in JSON format. This file details the involved consumer and provider and outlines the interactions between them, with each interaction consisting of a pair of expected request-response messages.

Additionally, in the response object, we can define matching rules that allow us to specify if we expect certain field types or even particular values.

The provider side also requires Pact tests, which mock responses for all APIs exposed to the consumer. Pact then validates these responses against the contract generated by the consumer, handling the validation process itself. This setup keeps the tests lightweight, eliminating the need for extra logic or complex configurations.

Furthermore, we aimed to ensure two additional key points. Since our project was in its early development phase, provider APIs were often created before the frontend began consuming them. To accommodate this scenario, we needed the provider side to generate tests for new APIs even if no consumer contract existed for them yet. Pact seamlessly supported this requirement, allowing the provider to add tests in advance. Consequently, once the consumer contract was available, no changes were needed on the provider side—assuming the API already aligned with consumer expectations.

Finally, we aimed to prevent any build if there was an API-breaking change. To achieve this, we integrated contract testing directly into our build pipeline. Although this decision coupled the frontend and backend build processes, it provided us with the assurance that no unhandled breaking changes would make their way into production. As with any architectural choice, this coupling came with trade-offs, but we accepted it for the added reliability it offered.

4 Key Advantages of Contract Testing

Contract testing presented several key advantages that aligned seamlessly with our project’s needs.

  • Early Detection of Inconsistencies and Breaking Changes
    By verifying contracts during development, we could identify inconsistencies early and catch breaking changes before they reached production. This proactive approach reduced the frequency of last-minute bug fixes and minimized the need for urgent communication between teams.
  • Support for Independent Development Cycles
    With a consumer-first approach, the frontend specified its requirements, enabling both frontend and backend teams to implement updates independently. This approach supported independent development cycles without the need for constant back-and-forth discussions, improving productivity and fostering a stronger sense of ownership within each team.
  • Automated CI/CD Integration
    Integrating contract tests into our CI/CD pipeline ensured that no new backend changes would be deployed unless they met the contract requirements. This automated verification provided peace of mind, allowing us to trust that both sides remained in sync before deployment, ultimately enhancing software quality.
  • Flexible and Effective Solution for Managing Changes
    Compared to generating TypeScript models, contract testing offered greater flexibility to manage changes without compromising alignment between frontend and backend components. This approach proved more effective in maintaining consistency across our system.

With these advantages, contract testing proved to be a more effective solution than our initial approach of generating TypeScript models. It offered the necessary flexibility to manage changes without compromising the alignment between frontend and backend components.

5 Common Pitfalls to Avoid

Through our experience with Pact, we discovered several important considerations when implementing contract testing. Here are some pitfalls and limitations to be aware of, which can help make your testing process smoother and more efficient.

  • Homogeneous vs. Heterogeneous Payloads
    Pact and similar contract testing tools handle homogeneous payload structures well, but testing heterogeneous payloads (such as arrays with mixed data types) can be inconvenient. Pact expects consistent structures, so mixed-type payloads require additional setup, making contracts harder to manage and maintain.
  • Avoid Functional Testing in Contract Tests
    Contract tests are intended to verify data structure compatibility between the consumer and provider, not to validate API behavior or logic. Using contract tests for functional testing adds complexity and can slow down CI/CD processes. Dedicated functional testing frameworks are better suited for testing API behavior, while contract tests should focus solely on compatibility.
  • Avoid Overly Strict Matchers
    Using strict matchers in contracts can make tests brittle, prone to failure with minor backend changes. Flexible matchers (e.g., type or regular expression matchers) allow for slight response variations without breaking compatibility, making contract tests more robust to small, non-breaking backend adjustments.
  • Limit Redundancy in Contracts
    Each contract should focus on a specific interaction between the frontend and backend. Avoiding duplicate or redundant contract tests reduces code clutter and speeds up the CI/CD pipeline by focusing on high-impact contracts essential to frontend-backend integration.
  • Validate Only Essential Fields
    Contracts should only validate fields the frontend depends on, limiting validation to fields that impact functionality. Including non-essential fields can lead to unnecessary test failures when minor changes occur. By focusing on key fields, contracts stay lean and manageable.

Keeping these pitfalls in mind, you can streamline your contract testing process and avoid common challenges. In our experience with Pact, adhering to these guidelines allowed us to implement a more efficient testing approach that reduced errors without overcomplicating our CI/CD workflows.

Lessons Learned and Future Considerations

Reflecting on our journey, we gained several critical insights that can be beneficial for other teams facing similar challenges. Defining contracts upfront to establish clear contracts early can save time and effort in the long run. It fosters clear communication between teams and sets the groundwork for smoother development processes.

Automating the testing process not only reduces the potential for human error but also enhances efficiency. Integrating contract testing into the CI/CD pipeline is a crucial step toward achieving this.

Maintaining open lines of communication to encourage collaboration between frontend and backend teams is essential. Regular check-ins and collaborative problem-solving can help mitigate misunderstandings and keep both sides aligned.

As projects evolve, so should processes and tools. To iterate on processes, regularly review workflows and remain open to new solutions that improve alignment and reduce friction. Embracing continuous improvement helps teams adapt to changing needs and enhance collaboration.

In conclusion, our experience with contract testing has been transformative. It not only addressed the pressing issue of alignment between frontend and backend systems but also enhanced team dynamics and overall productivity. As we continue to build and scale our project, we remain committed to refining our processes and leveraging contract testing to deliver high-quality software that meets our users’ needs.

Appendix: Pact in Angular Projects

When we started implementing Pact for contract testing in our Angular application, we encountered a limitation with the default Angular test runner, Karma. Pact requires a Node.js runtime to start and stop mock servers, write logs, and handle other critical operations. Unfortunately, browser-based environments like Karma are not equipped for these capabilities, as they lack the necessary Node runtime.

This issue meant that, to effectively run Pact tests, we needed to transition to a Node-based test runner, such as Jest or Mocha. After considering several options, we chose Jest for its strong integration with Node and Angular, as well as its fast, reliable testing capabilities.

Previously, there was support for using Karma with Pact, but that changed in June 2022, when Matt Fellows, co-founder of Pactflow, announced the retirement of support for Pact Web and Karma due to maintenance challenges. In his words:

“I’ve decided to retire support for Pact Web. At the moment, the burden of maintaining the extra interface, packages (e.g. Karma) and examples is just not worth it, when we have a first-class testing capability with Node JS-based packages.”

This change emphasized that Pact was moving forward with a Node-centric testing approach, making Jest, Mocha, and other Node-compatible test frameworks the preferred option for Pact tests. According to Pact’s documentation:

“Pact requires a Node runtime to be able to start and stop Mock servers, write logs and other things. However, when used within browser or non-Node based environments—such as with Karma or ng-test—this is not possible. You will need a Node based test framework such as Jest or Mocha.”

By configuring our Angular tests with Jest, we aligned with Pact’s evolving support and ensured smooth, reliable contract testing without compatibility issues. This setup allowed us to keep using Pact efficiently in our Angular workflow, providing the flexibility and robustness we needed for seamless frontend-backend integration.