
Research
/Security News
9 Malicious NuGet Packages Deliver Time-Delayed Destructive Payloads
Socket researchers discovered nine malicious NuGet packages that use time-delayed payloads to crash applications and corrupt industrial control systems.
@bbc/consumer-contracts
Advanced tools
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.
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 = require("@bbc/consumer-contracts").Contract;
var Joi = require("@bbc/consumer-contracts").Joi;
module.exports = new Contract({
name: "User API",
consumer: "My GitHub Service",
request: {
method: "GET",
url: "https://api.github.com/users/robinjmurphy",
},
response: {
status: 200,
body: Joi.object().keys({
login: Joi.string(),
name: Joi.string(),
public_repos: Joi.number().integer(),
}),
},
});
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
If you need to perform asynchronous operations to set up a contract then you can instead export a function that returns a Contract:
var Contract = require("@bbc/consumer-contracts").Contract;
var Joi = require("@bbc/consumer-contracts").Joi;
module.exports = async function () {
const username = await getUsername();
return new Contract({
name: "User API",
consumer: "My GitHub Service",
request: {
method: "GET",
url: `https://api.github.com/users/${username}`,
},
response: {
status: 200,
body: Joi.object().keys({
login: Joi.string(),
name: Joi.string(),
public_repos: Joi.number().integer(),
}),
},
});
};
Each contract contains four required properties; consumer, name, request and response.
var Contract = require("@bbc/consumer-contracts").Contract;
var Joi = require("@bbc/consumer-contracts").Joi;
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
request: {
// ...
},
response: {
// ...
},
});
consumerThe 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.
nameThe name property appears in the output of the consumer-contracts tool and helps you identify each individual contract.
requestThe 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.readFileSync('/path/to/my/cert.p12'),
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.
responseThe 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.
To require a specific HTTP status code, set the status property to that value:
response: {
status: 200;
}
To allow a range of different status codes, you can use Joi's valid() function:
response: {
status: Joi.any().valid(200, 201, 202);
}
The response headers can be validated using a Joi schema:
response: {
headers: Joi.object().keys({
Location: Joi.string().regex(/\/users\/\d+/),
"Content-Type": "application/json",
});
}
The response body can be validated using a Joi schema:
response: {
body: Joi.object().keys({
name: Joi.string(),
items: Joi.array().items(
Joi.object().keys({
id: Joi.number.integer(),
}),
),
});
}
client optionalYou 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 = require("@bbc/consumer-contracts").Contract;
var Joi = require("@bbc/consumer-contracts").Joi;
var client = require("request").defaults({
headers: {
authorization: "Bearer xxx",
},
});
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
request: {
// ...
},
response: {
// ...
},
client: client,
});
before optionalIf 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.
You can either pass a function that receives a callback argument which will be called when setup is complete:
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
before: function (done) {
// setup
done();
},
request: {
// ...
},
response: {
// ...
},
});
Or pass an asynchronous function that will be awaited on:
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
before: async function () {
// setup
},
request: {
// ...
},
response: {
// ...
},
});
after optionalIf your contract requires some cleanup you can use the after property. It takes a function that will be run after the contract executes.
You can either pass a function that receives a callback argument which will be called when cleanup is complete:
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
request: {
// ...
},
response: {
// ...
},
after: function (done) {
// cleanup
done();
},
});
Or pass an asynchronous function that will be awaited on:
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
request: {
// ...
},
response: {
// ...
},
after: async function () {
// cleanup
},
});
joiOptions optionalOverrides the default Joi validation options.
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
request: {
// ...
},
response: {
// ...
},
joiOptions: {
allowUnknown: false,
},
});
retries optionalRetries the contract if it fails. Can be specified in two ways:
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
request: {
// ...
},
response: {
// ...
},
retries: 2,
retryDelay: 3000 // optional, milliseconds between retries
});
module.exports = new Contract({
name: "Contract name",
consumer: "Consumer name",
request: {
// ...
},
response: {
// ...
},
retries: {
maxRetries: 3,
handler: (error, request, retryCount) => {
// Return false to stop retrying
// Return true to retry immediately
// Return a number to retry after that many milliseconds
// Example: Retry on 202 status with exponential backoff
if (error.statusCode === 202 && retryCount < 3) {
return Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
}
return false;
}
}
});
The retry handler receives:
error - The error object containing:
message - Error messagedetail - Detailed error informationstatusCode - HTTP status coderesponse - Full response objectrequest - Original request objectrequest - The original request optionsretryCount - Number of retries attempted so far (starting at 0)It should return:
false to stop retryingtrue to retry immediatelyThe max retries is 0 by default.
retryDelay optionalUsed with numeric retries to wait retryDelay milliseconds between each retry. Ignored when using a retry handler.
This defaults to 0.
Can be achived by returning an array of contracts.
module.exports = [
new Contract(),
// ...
new Contract(),
// ...
];
runTo 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
To validate an array of contracts programmatically, first require the validateContracts function:
var validateContracts = require("@bbc/consumer-contracts").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 = [
new Contract(...),
new Contract(...)
];
var handleContractValidations = function (err, res) { ... }
validateContracts(contracts, handleContractValidations);
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.
FAQs
Consumer driven contracts for Node.js
The npm package @bbc/consumer-contracts receives a total of 328 weekly downloads. As such, @bbc/consumer-contracts popularity was classified as not popular.
We found that @bbc/consumer-contracts demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 656 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Socket researchers discovered nine malicious NuGet packages that use time-delayed payloads to crash applications and corrupt industrial control systems.

Security News
Socket CTO Ahmad Nassri discusses why supply chain attacks now target developer machines and what AI means for the future of enterprise security.

Security News
Learn the essential steps every developer should take to stay secure on npm and reduce exposure to supply chain attacks.