Writing Pact Consumer Tests in TypeScript — consumer-driven contract testing with Pact for microservices.
Writing Pact Consumer Tests in TypeScript solves the integration testing problem in microservices architectures. Instead of running end-to-end tests against a full deployed environment, consumer-driven contracts let each service test its dependencies in isolation while maintaining compatibility guarantees.
1# Contract testing flow:
2
31. Consumer writes Pact test
4 → Interaction: POST /auth/login with {email, password}
5 → Expectation: 200 {accessToken: string, user: object}
6
72. Pact runs consumer test against a mock server
8 → Records the interaction as a pact file (JSON)
9
103. Pact publishes to Pact Broker
11 → Other teams can see what you depend on
12
134. Provider runs Pact verifier
14 → Replays each interaction against the real API
15 → Verifies responses match the consumer's expectations
16
175. can-i-deploy check before deployment
18 → Ensures consumer and provider are compatible1npm install --save-dev @pact-foundation/pact1import { PactV3 } from '@pact-foundation/pact';
2import { like, eachLike } from '@pact-foundation/pact/src/dsl/matchers';
3import * as path from 'path';
4
5const provider = new PactV3({
6 consumer: 'web-frontend',
7 provider: 'api-service',
8 dir: path.resolve(process.cwd(), 'pacts'),
9});
10
11test('GET /courses contract', async () => {
12 await provider
13 .given('courses exist')
14 .uponReceiving('a request for courses')
15 .withRequest({ method: 'GET', path: '/courses' })
16 .willRespondWith({
17 status: 200,
18 body: {
19 data: eachLike({ id: like('id'), title: like('title') }),
20 meta: like({ total: 42 }),
21 },
22 })
23 .executeTest(async (mockServer) => {
24 const res = await fetch(`${mockServer.url}/courses`);
25 const body = await res.json();
26 expect(res.status).toBe(200);
27 expect(body.data).toBeInstanceOf(Array);
28 });
29});1import { Verifier } from '@pact-foundation/pact';
2
3test('provider verification', async () => {
4 await new Verifier({
5 providerBaseUrl: `http://localhost:${port}`,
6 pactUrls: ['pacts/web-frontend-api-service.json'],
7 provider: 'api-service',
8 stateHandlers: {
9 'courses exist': async () => {
10 await prisma.course.createMany({ data: testCourseData });
11 },
12 },
13 }).verifyProvider();
14});1# Publish pacts after consumer tests pass:
2npx pact-broker publish pacts/ \
3 --broker-base-url https://pact-broker.example.com \
4 --broker-token $TOKEN \
5 --consumer-app-version $(git rev-parse --short HEAD)
6
7# Check deployment safety:
8npx pact-broker can-i-deploy \
9 --pacticipant web-frontend \
10 --version $(git rev-parse --short HEAD) \
11 --to-environment staging \
12 --broker-base-url https://pact-broker.example.com
13
14# can-i-deploy exits 0 (safe) or 1 (not safe)
15# Add to deploy script before every deployment1import { like, eachLike, term, integer, decimal } from '@pact-foundation/pact/src/dsl/matchers';
2
3// like(): matches the type, not the value
4body: { id: like('any-string-id'), count: like(42) }
5// Verifies: id is a string, count is a number
6
7// eachLike(): array where each item matches the pattern
8body: { data: eachLike({ id: like('id'), price: like(10.0) }) }
9// Verifies: data is an array, each item has id (string) and price (number)
10
11// term(): regex match
12body: { email: term({ generate: '[email protected]', matcher: '^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$' }) }
13
14// integer(): whole numbers only
15body: { count: integer(5) }
16
17// decimal(): decimal numbers
18body: { price: decimal(79.99) }
19
20// NEVER use exact values in pact matchers:
21// BAD: body: { id: 'cuid-specific-value' } — breaks when ID changes
22// GOOD: body: { id: like('any-id') } — stable across test runsFull Pact implementation with consumer test, provider verification, and Pact Broker integration.
1// Writing Pact consumer tests in TypeScript
2import { PactV3, MatchersV3 } from '@pact-foundation/pact';
3import { like, eachLike } from '@pact-foundation/pact/src/dsl/matchers';
4import * as path from 'path';
5
6const provider = new PactV3({
7 consumer: 'web-frontend',
8 provider: 'api-service',
9 dir: path.resolve(process.cwd(), 'pacts'),
10 logLevel: 'warn',
11});
12
13describe('API consumer contract', () => {
14 it('GET /courses returns a list of courses', async () => {
15 await provider
16 .given('courses exist')
17 .uponReceiving('a request to list courses')
18 .withRequest({ method: 'GET', path: '/courses', query: { limit: '10', page: '1' } })
19 .willRespondWith({
20 status: 200,
21 headers: { 'Content-Type': 'application/json; charset=utf-8' },
22 body: {
23 data: eachLike({
24 id: like('cuid-abc123'),
25 title: like('TypeScript Mastery'),
26 price: like(79.99),
27 rating: like(4.8),
28 }),
29 meta: like({ total: 42, page: 1, limit: 10 }),
30 },
31 })
32 .executeTest(async (mockServer) => {
33 const response = await fetch(`${mockServer.url}/courses?limit=10&page=1`);
34 const data = await response.json();
35 expect(response.status).toBe(200);
36 expect(data.data).toBeInstanceOf(Array);
37 expect(data.data[0]).toHaveProperty('id');
38 expect(data.data[0]).toHaveProperty('title');
39 });
40 });
41
42 it('POST /auth/login returns access token', async () => {
43 await provider
44 .given('user exists with email [email protected]')
45 .uponReceiving('a valid login request')
46 .withRequest({
47 method: 'POST',
48 path: '/auth/login',
49 headers: { 'Content-Type': 'application/json' },
50 body: { email: '[email protected]', password: 'Pass123!' },
51 })
52 .willRespondWith({
53 status: 200,
54 body: {
55 accessToken: like('eyJhbGci...'),
56 user: like({ id: 'user-123', email: '[email protected]' }),
57 },
58 })
59 .executeTest(async (mockServer) => {
60 const res = await fetch(`${mockServer.url}/auth/login`, {
61 method: 'POST',
62 headers: { 'Content-Type': 'application/json' },
63 body: JSON.stringify({ email: '[email protected]', password: 'Pass123!' }),
64 });
65 expect(res.status).toBe(200);
66 const body = await res.json();
67 expect(typeof body.accessToken).toBe('string');
68 });
69 });
70});This implementation demonstrates writing pact consumer tests in typescript with the complete Pact workflow. Consumer tests run against a mock server (no real API needed), recording interactions as pact files. Provider verification replays those interactions against the real NestJS API with state handlers setting up required test data. The Pact Broker stores contracts and the can-i-deploy command checks compatibility before every deployment.
Continue with the next contract testing topic to complete your consumer-driven contract testing implementation.
Your progress is saved in this browser, on this device only.