Microservices & Contract Testing — The Blueprint
When backends and frontends evolve separately, APIs break silently. Learn how to use Playwright and Zod to perform Contract Testing, catching breaking schema changes before they destroy your UI.
📖 The Story: The Architecture Avalanche
A growing fintech startup split their monolith into 10 different microservices. The Frontend team and the Backend teams moved independently. Everything seemed fine.
One Tuesday, the "User Profile" service pushed an update. They renamed the JSON field phoneNumber to contactNumber to be more consistent. The backend tests passed. The API returned 200 OK.
But the frontend wasn't updated. When users logged in, the profile page read user.phoneNumber, which was now undefined. The UI displayed "Phone: undefined". Users panicked, thinking their accounts were corrupted. The startup rolled back the deployment.
The QA team investigated. Their E2E tests mocked the API response using the old schema (phoneNumber), so they didn't catch the change. The backend unit tests didn't care about the frontend. There was a gap.
The QA lead implemented Contract Testing. They defined a Zod schema representing exactly what the frontend expected. They added a Playwright test that hit the real staging API and parsed the response through the schema. The next time the backend renamed the field, the Playwright test failed instantly in CI: Expected 'phoneNumber', received 'contactNumber'. The avalanche was stopped.
In a microservices world, 200 OK means nothing if the payload shape changes. The schema is the contract.
🎯 Why Contract Testing?
Enforce Agreements
Ensure the backend sends exactly what the frontend expects, down to the data type.
Catch Silent Breaks
A 200 OK with a missing field is worse than a 500 Error. Schemas catch this.
Independent Deploys
Teams can deploy with confidence knowing they haven't broken the contract.
Fast Feedback
Schema validation is instant. No need to render the entire UI to find a missing field.
🌍 Real World Example
A frontend expects an array of orders. The backend, due to a bug, returns an object { orders: [...] } instead of the array directly. The frontend's map() function crashes. A Playwright contract test intercepts the API response and asserts it is an Array. The test fails in CI before the code reaches staging.
🧒 Explain Like I'm 10
Imagine two builders making parts of a house. Builder A makes a door. Builder B makes the door frame. They both have a blueprint (the contract).
If Builder A decides to make the door 6 feet tall, but Builder B makes the frame 5 feet tall, the door won't fit. They both followed their own ideas, but they didn't follow the same blueprint.
Contract testing is checking both the door and the frame against the exact same blueprint before trying to put them together.
🎓 Professional Explanation
Contract Testing validates the communication between services. In a frontend-driven architecture, we use "Consumer-Driven Contract Testing" (CDCT). The frontend defines a schema (using tools like Zod, Ajv, or JSON Schema) representing the exact structure of the API response it needs. Playwright intercepts the real API response (or makes a direct API call) and parses it through the schema. If the backend changes a field type, omits a required property, or adds an unexpected structure, the schema validator throws a precise error, failing the test.
📊 Microservices Architecture
The Gap in Traditional E2E Testing
Frontend (Consumer)
Expects user.name (String) and user.age (Number).
API (Contract)
The blueprint. If the API changes age to a String, the contract breaks.
Backend (Provider)
Sends JSON. Often updated independently by a different team.
Without Contract Testing, Playwright mocks the API, completely blind to backend changes.
📦 Step 1: Installing Zod
Zod is a TypeScript-first schema declaration and validation library. It is perfect for defining API contracts in Playwright.
npm install --save-dev zod
📜 Step 2: Defining the Schema (The Contract)
Define exactly what the frontend expects. Be strict.
import { z } from 'zod'; // The Contract export const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), phoneNumber: z.string(), // Must exist! If backend sends contactNumber, this fails. isActive: z.boolean(), roles: z.array(z.string()) }); // Type for TypeScript autocompletion export type User = z.infer<typeof UserSchema>;
🪝 Step 3: Intercepting & Validating in Playwright
Now, use Playwright to hit the real API (or intercept the response during an E2E test) and validate it against the Zod schema.
import { test, expect } from '@playwright/test'; import { UserSchema } from '../schemas/userSchema'; test('GET /api/users/1 matches the frontend contract', async ({ request }) => { // 1. Make the real API request const response = await request.get('/api/users/1'); expect(response.status()).toBe(200); // 2. Parse the JSON const json = await response.json(); // 3. Validate against the Schema (The Contract Test) const validationResult = UserSchema.safeParse(json); // 4. Assert the contract is valid. If it fails, print the exact Zod error. expect(validationResult.success, validationResult.error?.message).toBe(true); });
Why safeParse? If you use UserSchema.parse(json), Zod will throw an error if the schema is invalid. While this fails the test, using safeParse allows you to capture the error object and pass its detailed message to Playwright's expect(). This makes the CI logs perfectly readable, showing exactly which field broke the contract.
⚠️ Common Mistakes
Mistake 1: Validating Only During Mocking
If you only use schemas to generate mock data, you aren't testing the real backend. Contract testing requires hitting the real staging API to ensure the backend hasn't drifted from the agreed schema.
Mistake 2: Overly Strict Schemas
Don't fail the test if the backend adds a new optional field (e.g., faxNumber). Zod by default ignores unknown keys. Don't use .strict() unless you absolutely want to prevent the backend from adding any new fields.
✅ Best Practices
- Consumer-Driven. The frontend team should write the schema. They know what they need.
- Share Schemas. If possible, share the Zod schema file with the backend team so they can use it for their own TypeScript validation.
- Use
safeParse. Capture Zod errors and pass them to Playwright'sexpectfor beautiful CI logs. - Run Contract Tests on every PR. They are incredibly fast (just API calls) and catch breaking changes instantly.
🏗️ Senior Engineer Deep Dive
Contract Testing vs. E2E Testing
Contract tests are much faster and more targeted than E2E tests. An E2E test might take 10 seconds to render the UI and check if the phone number displays. A contract test takes 50ms to hit the API and validate the JSON. You should have hundreds of contract tests and only a few dozen E2E tests.
Pact vs. Zod
Pact is a dedicated contract testing framework where the consumer generates a "pact" file, and the provider verifies against it. While powerful, it requires a Pact Broker. Using Zod inside Playwright is a lightweight alternative that achieves 90% of the value (schema validation) without the infrastructure overhead, making it ideal for teams already invested in Playwright.
💼 Interview Questions
Show Answer
Show Answer
request fixture to hit the real API, parse the JSON, and pass it to Zod.safeParse(). I then assert that the validation was successful.safeParse instead of parse in Zod during a Playwright test?Show Answer
parse fails, it throws a raw ZodError, which Playwright catches and displays as a generic test failure. safeParse returns an object { success, error }. I can pass the detailed error.message directly into Playwright's expect() as a custom message, making the CI log show exactly which field broke the contract.internal_uuid, to the User API. Your contract test fails. How do you handle this?Show Answer
.strict() on the schema object. I would evaluate if the frontend needs this field. If not, I would remove .strict() to allow the backend to add optional fields without breaking the consumer's tests, ensuring backward compatibility.🏋️ Practical Exercise
Validate a Public API
- Install Zod in your Playwright project.
- Write a test that calls
https://jsonplaceholder.typicode.com/users/1. - Define a Zod schema expecting
id(number),name(string), andemail(string). - Use
safeParseto validate the response. - Assert the validation is successful.
🚀 Mini Project
🏗️ The Breaking Change Detector
- Write a Playwright test that hits a real API and validates it with Zod.
- Intentionally change your Zod schema to expect
phoneNumber(which doesn't exist in the API). - Run the test. Observe how the CI log clearly shows the Zod error: "Expected phoneNumber, received undefined".
- Revert the schema to pass the test.
📝 Quiz
Test your understanding. Click an option to check your answer.
safeParse() instead of parse() in Zod?parse() is deprecated.safeParse() returns an object with error details, allowing you to pass them to expect() for better logs.safeParse() is faster.parse() doesn't validate arrays.safeParse prevents raw throws and lets you format the error message for CI.age: 30 (Number) to age: "30" (String), what will the Zod schema z.number() do?z.number() will reject a string unless explicitly coerced.❓ FAQ
Q: Can I use this for GraphQL APIs?
A: Yes! GraphQL APIs return JSON. You can intercept the GraphQL response and validate it against a Zod schema exactly the same way.
Q: What if my API returns a 500 error?
A: Your Playwright test should first assert the status code is 200 OK. If it's 500, the test fails before Zod validation even runs.
📦 Summary
🎯 Key Takeaways
- 200 OK doesn't mean the API is safe. Payload structure matters.
- Contract Testing ensures the backend and frontend agree on the JSON schema.
- Use Zod to define strict TypeScript schemas for your API responses.
- Use
safeParseto capture errors and format them for Playwright CI logs. - Consumer-Driven: The frontend team defines the schema.
- Contract Tests are fast and should run on every PR.
