Featured project 9 · TypeScript + Java · Contract testing
Pact Contract Testing
Consumer-driven contract testing with Pact between a TypeScript checkout client and a Java Spring Boot product API, through a self-hosted Pact Broker with can-i-deploy, in one command locally and in GitHub Actions.
Purpose
Shows how two teams can change and deploy their services independently without an end-to-end environment: the consumer writes down what it needs, the provider proves it delivers that, and the broker answers before every deployment whether the versions fit together. Two languages on purpose: the teams share only the contract.
Technology stack
| Area | Tools |
|---|---|
| Consumer | TypeScript, fetch, Pact JS 17 (Pact specification V4), Vitest |
| Provider | Java 17, Spring Boot 4.1, Pact JVM 4.7 with JUnit 5, Maven wrapper |
| Broker | Self-hosted Pact Broker 2.121 with PostgreSQL 17 in Docker Compose |
| Deployment checks | pact-broker CLI: publish, can-i-deploy, create-environment, record-deployment |
| CI | GitHub Actions: consumer and provider jobs, then the full broker workflow with the matrix on the run page |
Architecture overview
checkout-web (TypeScript) contract tests run the real client against Pact's mock server
│ pact file (6 interactions)
▼
Pact Broker (Docker) contracts, verification results, branches, environments, deployments
│ can-i-deploy? record-deployment
▼
product-service (Spring Boot) JUnit 5 verification of the real app: provider states, token filterKey engineering decisions
- The consumer's contract tests run its real HTTP client, so the contract records what the code really sends
- Only the fields the consumer uses are in the contract, with type and format matchers; the provider may change the rest
- Provider states with parameters ("a product exists" with id, price and stock) instead of one state per example
- The contract accepts any Bearer token; the provider's request filter injects a valid one, but leaves the no-token case alone
- Pending and WIP pacts: a consumer's new expectation never breaks the provider's build; can-i-deploy blocks the consumer instead
- The breaking-change demo checks why can-i-deploy said no, so a no for another reason cannot pass as success
Implemented features
- 6 interactions: one product, 404, products by ids, reservation 201, 409 with the available stock, 401 without a token
- Provider verification from the pacts folder (local) or the broker (CI), chosen by one environment variable
- npm run contract: broker, consumer tests, publish, provider verification with results, can-i-deploy and record-deployment in about 35 seconds
- npm run demo:breaking: a consumer feature branch expects a field the provider lacks; the provider stays green, the consumer is blocked
- Mutation checks: renaming a field in the provider fails verification with the missing key named
- Framework design notes and 76 interview questions on contract testing and Pact, linked to the code (Markdown + PDF)
Setup and execution
npm ci && npm ci --prefix checkout-web
npm run contract # the whole workflow (needs Docker and Java 17+)
npm run demo:breaking # a breaking consumer change, blocked by can-i-deploy
npm run broker:downSample test results


CI workflow
On every push and pull request: the consumer job (lint, types, unit and contract tests) and the provider job (Maven tests) in parallel, then a job that starts the Pact Broker with Docker Compose and runs the whole workflow and the breaking-change demo, and writes the broker's matrix to the run page.
Known limitations and future improvements
Limitations
- One consumer; message (event) contracts are not included yet
- The broker in CI is new for every run and has no authentication (a shared broker needs both)
Next steps
- A second consumer and a message pact for an event
- A broker webhook that starts the provider verification, and can-i-merge on pull requests
