Consumer-driven contracts in JavaScript
Consumer-driven contracts let you move fast without breaking things.
API consumers codify their expections of your service in an executable contract. This defines the type of response that they expect for a given request. Contracts give you an insight into which parts of your API clients depend on, and which parts can be changed without fear of breaking them.
This project lets you write executable contracts in JavaScript. It uses request to make HTTP requests and Joi to validate API responses. Contracts are defined as JavaScript modules in a contracts
directory at the root of your project and can be executed using the consumer-contracts
tool.
Getting started
Install the consumer-contracts
tool globally:
npm install --global consumer-contracts
Install the consumer-contracts
module locally (this gives you access to the contract definition interface in your contract files):
npm install --save-dev consumer-contracts
Create a contracts
directory at the root of your project:
mkdir contracts
Create a JavaScript file within the contracts
directory for your first contract. The example below is a contract for the GitHub User API, we'll call it user-api.js
. In this example, the consumer depends on the login
, name
and public_repos
properties returned in the response body.
var Contract = Contract;var Joi = Joi; moduleexports = name: 'User API' consumer: 'My GitHub Service' request: method: 'GET' url: 'https://api.github.com/users/robinjmurphy' response: statusCode: 200 body: Joiobject ;
To validate the contract, run the following command at the root of your project directory:
consumer-contracts run
You should see that the contract validates:
✓ My GitHub Service – User API
1 passing
Anatomy of a contract
Each contract contains four required properties; consumer
, name
, request
and response
.
var Contract = Contract;var Joi = Joi; moduleexports = name: 'Contract name' consumer: 'Consumer name' request: // ... response: // ... ;
consumer
The consumer
property appears in the output of the consumer-contracts
tool and should be the name of the consuming service that the contract applies to.
name
The name
property appears in the output of the consumer-contracts
tool and helps you identify each individual contract.
request
The request
property defines the HTTP request that the contract applies to. All of the options supported by request are valid. This means you can specify headers and SSL configuration options (among other things) for a given request:
request: method: 'GET' url: 'https://exmaple.com/users/fred' headers: Accept: 'text/xml' pfx: fs passphrase: 'my-cert-passphrase'
When running under certain environments you may receive SSL errors regarding a CA file. If you know that strict trust checking is not required in this situation you may set strictSSL: false
on the above request object, which should resolve the issue.
response
The response
object validates the response returned by your service. The entire object is treated as a Joi schema that validates the res
object returned by request
. This means that the response's status code, headers and JSON body can all be validated using Joi's flexible schema language. The following default options are passed to Joi's validate()
function:
allowUnknown: true presence: 'required'
This means that any fields you choose to validate are required by default. To indicate that a field is optional, use the optional()
modifier.
If you need to override the default Joi options, you can use the optional joiOptions
property in your contract.
Validating the response code
To require a specific HTTP status code, set the statusCode
property to that value:
response: statusCode: 200
To allow a range of different status codes, you can use Joi's valid()
function:
response: statusCode: Joi
Validating the response headers
The response headers can be validated using a Joi schema:
response: headers: Joiobject
Validating the response body
The response body can be validated using a Joi schema:
response: body: Joiobject
client
optional
You can use a pre-configured request client for your contracts using the client
property. This can be useful when you have a set of common request options across contracts.
var Contract = Contract;var Joi = Joi;var client = ; moduleexports = name: 'Contract name' consumer: 'Consumer name' request: // ... response: // ... client: client;
before
optional
If your contract requires some setup (e.g. populating an API with data) you can use the before
property. It takes a function that will be run before the contract executes. The setup function receives a callback argument that you should call once your setup is complete.
moduleexports = name: 'Contract name' consumer: 'Consumer name' { // setup ; } request: // ... response: // ... ;
after
optional
If your contract requires some cleanup you can use the after
property. It takes a function that will be run after the contract executes. The after function receives a callback argument that you should call once your cleanup is complete.
moduleexports = name: 'Contract name' consumer: 'Consumer name' request: // ... response: // ... { // cleanup ; };
joiOptions
optional
Overrides the default Joi validation options.
moduleexports = name: 'Contract name' consumer: 'Consumer name' request: // ... response: // ... joiOptions: allowUnknown: false ;
CLI
run
To validate all of the contracts in the contracts
directory, type:
consumer-contracts run
This works recursively, which means you can keep the contracts for each of your consumers in a separate subdirectory.
To run a single contract, pass a filename to the run
command:
consumer-contracts run ./contracts/consumer-a/contract-1.js
Programmatic Usage
To validate an array of contracts programmatically, first require the validateContracts
function:
var validateContracts = validateContracts;
The validateContracts
function can then be called with an array of contracts and a callback function which takes two arguments,
error
and results
:
var contracts = ... ...; var { ... } ;
The error
argument will always be null, as consumer-contracts
will always run every contract in the array rather than failing fast, as such, error handling must deal with the err
field of each object in the results array as detailed below.
The results
will be an array of objects with fields contract
and err
. The contract
field of the result object contains the executed Contract object including any before
and after
fields. The err
field contains any error that occurred when validating the specific contract. Error handling should check the err
field of every result object is null
before declaring the contract suite as having been run successfully.