Thanks to the rollout of Twilio's OpenAPI specification, it is now possible to mock Twilio's API locally and in your CI environments. Using a mock can save you time and money during development and testing.
A mock API server allows you to emulate a real API (such as Twilio) locally, in your test environments, and/or even publicly. This mock server can receive requests, and will respond to them with static or dynamic data that simulates what the real API would return.
Developing and/or testing against a mock API server has a litany of benefits, including but not limited to:
Additionally, because the mock server in this example will be generated using Twilio's OpenAPI Specification, each endpoint will validate requests and return data in the same format as the live Twilio API.
If you're interested in taking advantage of these benefits, let's proceed and learn how to set up the mock.
In order to generate a mock server from Twilio's OpenAPI specification, you will use the open source tool, Prism.
What is Prism? According to the Prism documentation:
Prism is an open-source HTTP mock server that can mimic your API's behavior as if you already built it. Mock HTTP servers are generated from your OpenAPI v2/v3 (formerly known as Swagger) documents.
Install Prism using your preferred package manager as shown below:
1npm install -g @stoplight/prism-cli23# OR45yarn global add @stoplight/prism-cli
Now that Prism is installed, the next step is to feed a specification file to Prism so that it can generate the desired mock API server. Run the following command:
prism mock https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json
Note that we're referencing the JSON spec files in our examples. It's also perfectly valid to use the YAML files instead, but for the sake of this article we will only be focusing on JSON.
After a short time you will see a trace similar to below. Once you see the green start
line the server is running and listening for requests on the displayed port on localhost
. Congratulations on starting your (maybe first) Prism mock server!
1$ prism mock https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json23› [CLI] … awaiting Starting Prism…4› [CLI] ℹ info GET http://127.0.0.1:4010/2010-04-01/Accounts.json?FriendlyName=omnis&Status=suspended&PageSize=8015› [CLI] ℹ info POST http://127.0.0.1:4010/2010-04-01/Accounts.json6› [CLI] ℹ info GET http://127.0.0.1:4010/2010-04-01/Accounts/ACbC8dB063DcfAe78a6D0cfEb09e0c7c7a/Addresses.json?CustomerName=doloribus&FriendlyName=porro&IsoCountry=necessitatibus&PageSize=3257…8› [CLI] ℹ info GET http://127.0.0.1:4010/2010-04-01/Accounts/ACF5ce8F97eb3E0270a856d8539e61DAE5.json9› [CLI] ℹ info POST http://127.0.0.1:4010/2010-04-01/Accounts/AC61FCB0720E02eCAf7fEeCbafEa602aE5.json10› [CLI] ▶ start Prism is listening on http://127.0.0.1:4010
To validate that this is working as expected, let's make some requests to the mock using curl
in various forms to see some features of the mock in action.
First, leave the Prism mock server running in the terminal, and open a new tab or window depending on what's allowed by your terminal. In the new terminal instance, let's start with the example of querying the Twilio API for a list of messages sent by your account. If we try the following command and forget to include credentials, we will get the following error:
curl -s -D "/dev/stderr" http://127.0.0.1:4010/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Messages.json | json_pp
Which will result in:
1HTTP/1.1 401 Unauthorized2Access-Control-Allow-Origin: *3Access-Control-Allow-Headers: *4Access-Control-Allow-Credentials: true5Access-Control-Expose-Headers: *6content-type: application/problem+json7WWW-Authenticate: Basic realm="*"8Content-Length: 3279Date: Wed, 31 Mar 2021 18:41:14 GMT10Connection: keep-alive11Keep-Alive: timeout=51213{14"headers" : {15"WWW-Authenticate" : "Basic realm=\"*\""16},17"title" : "Invalid security scheme used",18"detail" : "Your request does not fullfil the security requirements and no HTTP unauthorized response was found in the spec, so Prism is generating this error for you.",19"status" : 401,20"type" : "https://stoplight.io/prism/errors#UNAUTHORIZED"21}
Also, if you take a peek at the Prism terminal instance, you will see some validation errors as below:
1› [HTTP SERVER] get /2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Messages.json ℹ info Request received2› [NEGOTIATOR] ℹ info Request contains an accept header: */*3› [VALIDATOR] ⚠ warning Request did not pass the validation rules4› [NEGOTIATOR] ⬤ debug Unable to find a response definition5› [NEGOTIATOR] ⬤ debug Unable to find a 'default' response definition6› [HTTP SERVER] get /2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Messages.json ✖ error Request terminated with error: https://stoplight.io/prism/errors#UNAUTHORIZED: Invalid security scheme used
Notice here that the mock API server is actually validating the content of our request as the real Twilio API would, and is throwing a 401
error since credentials were not included in the request headers. Let's fix that and try again once we've set some environment variables for Twilio credentials
1export TWILIO_ACCOUNT_SID=AC…2export TWILIO_AUTH_TOKEN=...34curl -s -D "/dev/stderr" http://127.0.0.1:4010/2010-04-01/Accounts/MockSID/Messages.json -u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN | json_pp
And the result will be:
1HTTP/1.1 422 Unprocessable Entity2Access-Control-Allow-Origin: *3Access-Control-Allow-Headers: *4Access-Control-Allow-Credentials: true5Access-Control-Expose-Headers: *6content-type: application/problem+json7Content-Length: 5088Date: Wed, 31 Mar 2021 18:41:43 GMT9Connection: keep-alive10Keep-Alive: timeout=51112{13"title" : "Invalid request",14"detail" : "Your request is not valid and no HTTP validation response was found in the spec, so Prism is generating this error for you.",15"status" : 422,16"validation" : [17{18"message" : "should NOT be shorter than 34 characters",19"severity" : "Error",20"code" : "minLength",21"location" : [22"path",23"accountsid"24]25},26{27"code" : "pattern",28"location" : [29"path",30"accountsid"31],32"message" : "should match pattern \"^AC[0-9a-fA-F]{32}$\"",33"severity" : "Error"34}35],36"type" : "https://stoplight.io/prism/errors#UNPROCESSABLE_ENTITY"37}
Note that this time the mock API server successfully validated the Account SID (in this case, MockSID
) and found that it failed validation due to being the wrong length and for including invalid characters. The mock is doing a great job of emulating the real Twilio API so far!
If we cut the shenanigans and give the mock API server valid input and see what happens. Run the following command:
curl -s -D "/dev/stderr" http://127.0.0.1:4010/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Messages.json -u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN | json_pp
and the output will be:
1HTTP/1.1 200 OK2Access-Control-Allow-Origin: *3Access-Control-Allow-Headers: *4Access-Control-Allow-Credentials: true5Access-Control-Expose-Headers: *6Content-type: application/json7Content-Length: 5518Date: Wed, 31 Mar 2021 18:43:18 GMT9Connection: keep-alive10Keep-Alive: timeout=51112{13"uri" : "http://example.com",14"next_page_uri" : "http://example.com",15"start" : 0,16"first_page_uri" : "http://example.com",17"messages" : [18{19"account_sid" : null,20"num_segments" : null,21"uri" : null,22"subresource_uris" : null,23"to" : null,24"direction" : "inbound",25"body" : null,26"status" : "queued",27"num_media" : null,28"messaging_service_sid" : null,29"api_version" : null,30"error_code" : null,31"from" : null,32"error_message" : null,33"date_updated" : null,34"date_sent" : null,35"date_created" : null,36"sid" : null,37"price_unit" : null,38"price" : null39}40],41"previous_page_uri" : "http://example.com",42"page_size" : 0,43"page" : 0,44"end" : 045}
Success! This caused the mock server to return a 200 status as well as a JSON representation of a messages sent record. The Prism terminal instance should also reflect this success as shown:
1› [HTTP SERVER] get /2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Messages.json ℹ info Request received2› [NEGOTIATOR] ℹ info Request contains an accept header: */*3› [VALIDATOR] ✔ success The request passed the validation rules. Looking for the best response4› [NEGOTIATOR] ✔ success Found a compatible content for */*5› [NEGOTIATOR] ✔ success Responding with the requested status code 200
Now that we've validated the mock API server is running and acting as expected, the next step is to make it so that our applications can communicate with the mock instead of sending requests to Twilio during development and testing.
In order for you to leverage the Prism mock server during development of your application code, your Twilio client will need to be configured to redirect requests to the mock instead of Twilio itself. We'll demonstrate how to accomplish this in Node.js below, but note that this is supported by all of the Twilio SDKs.
As mentioned in the Twilio Node documentation on custom HTTP clients, it's possible to extend the behavior of the Twilio client by creating and passing in a custom implementation of the httpClient
property. This is a perfect point in the codebase to add some code that will intercept requests and redirect them to the mock.
Starting from within a new empty directory, create a file for housing your custom Request Client. In this case let's call it PrismClient.js
.
Next, add the following example code:
1class PrismClient {2constructor(prismUrl, requestClient) {3this.prismUrl = prismUrl;4this.requestClient = requestClient;5}67request(opts) {8opts.uri = opts.uri.replace(/^https\:\/\/.*?\.twilio\.com/, this.prismUrl);9return this.requestClient.request(opts);10}11}1213exports.PrismClient = PrismClient;
This PrismClient
has a few key points:
request
method uses a regular expression to change the URL of any request intended for Twilio to instead hit the same relative path, but on the Prism server instead.To use this new client, let's create an index.js
in the same directory that sends a new SMS Message. Additionally, let's make sure the necessary dependencies are also available, such as the Twilio Node SDK:
1yarn init -y2# Or if you prefer npm3npm init -y45# Once initialized, install dependencies!6yarn add twilio dotenv7# Or with npm8npm i twilio dotenv910# Create a new js file as well as a safe file for credentials and other sensitive variables to live in11touch index.js .env
Next, paste the following example code into index.js
1require('dotenv').config();23const twilio = require('twilio');4const { PrismClient } = require('./PrismClient');56const { RequestClient } = twilio;78const apiKey = process.env.TWILIO_API_KEY;9const apiSecret = process.env.TWILIO_API_SECRET;10const accountSid = process.env.TWILIO_ACCOUNT_SID;11const to = process.env.TO_NUMBER;12const from = process.env.FROM_NUMBER;1314const client = twilio(apiKey, apiSecret, {15accountSid,16httpClient: new PrismClient('http://127.0.0.1:4010', new RequestClient()),17});1819client.messages20.create({ to, from, body: 'Ahoy!' })21.then((message) => console.log(`Message ${message.sid} sent!`))22.catch((error) => console.error(error));23
Note that we're using best practices here, and creating the Twilio client using an API Key and Secret instead of directly using and potentially exposing your account's auth token. If you'd like to learn more about API Keys and how to create one, please refer to the API Keys documentation!
There are a few important things happening here of note:
twilio
, the custom PrismClient
that we just created earlier, and destructuring out twilio
s internal RequestClient
.httpClient
as an option. The first argument is the URL and port of the locally running Prism server, and the second is the Twilio RequestClient
that was imported earlier.Similarly, add the following variables to your .env
file, being sure to use your own valid Account SID, API Key, Key Secret, and Twilio/Personal phone numbers.
1# .env2TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX3TWILIO_API_KEY=SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX4TWILIO_API_SECRET=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX5TO_NUMBER=+123456789016FROM_NUMBER=+12345678901
Execute the example code by running node index.js
.
Once execution completes, you'll see the following in your terminal:
1$ node example.js2Message null sent!
Don't be alarmed by the null
; this is a result of Prism successfully intercepting the request and returning null
for the message sid
!
If you switch over to the terminal that is running Prism, you will see logs indicating a successful request/response cycle from Prism that returned a 201:
1› [HTTP SERVER] post /2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Messages.json ℹ info Request received2› [NEGOTIATOR] ℹ info Request contains an accept header: application/json3› [VALIDATOR] ✔ success The request passed the validation rules. Looking for the best response4› [NEGOTIATOR] ✔ success Found a compatible content for application/json5› [NEGOTIATOR] ✔ success Responding with the requested status code 201
This is an example of being able to run your code locally and verify that your integration with Twilio is correct. Note that the request to create and send an SMS never reached Twilio (you shouldn't have received a text!), and instead was quickly and safely redirected to the mock instead.
If your code uses the .list
or .each
methods that are available (such as client.messages.list())
, running your code against the mock will cause it to throw an error. This is because the mocked next_page_uri
value will be http://example.com
, which will cause the request to 404 against Prism.
If your application makes calls to other subdomains than api.twilio.com
, this mocking solution will not intercept calls to those other subdomains yet. It's possible to duplicate this process for other files one at a time (for example, mocking for accounts.twilio.com
but not api.twilio.com
), however we are still developing a solution to allow mocking for multiple subdomains at once.
If you want to share this mock with other developers on your team without the need for them to install Stoplight, or make it reproducible in another environment such as your CI pipeline, running it as a Docker container is highly advised. If you do not have Docker already installed, you can download and install it from here.
To run the previous mock API server using the Stoplight Docker image instead of the Stoplight CLI, run the following example command. Be sure to end any previously running Prism processes, otherwise the port will already be taken and the Docker container will fail to start:
docker run --init -d -p 4010:4010 stoplight/prism:4 mock -h 0.0.0.0 https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json
Once the Docker container is built (it may take some time depending on connection speeds), it will begin running and accepting requests on localhost:4010
just as before with the CLI.
You can verify that your container is running using the following command and looking for the stoplight/prism:4
image as below:
docker ps
You may also leverage the Docker Dashboard for a GUI-based way of viewing the running container and its logs.
Now that you have a successfully running, Dockerized version of the Prism mock it's time to verify it is working as expected. Run the node example again via node example.js
, and once again you should see:
1$ node example.js2Message null sent!
If you consult the Docker Dashboard for your container, you should see logs indicating that a message request was received and that a 201 was returned, just as we saw for the non-Dockerized version of Prism.
When you want to stop your container, first list out the running Docker images, then identify either the Container ID or Name of the running Prism container.
1$ docker ps2CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES3c1f2b80c13ae stoplight/prism:4 "node dist/index.js …" 3 seconds ago Up 2 seconds 0.0.0.0:4010->4010/tcp, :::4010->4010/tcp youthful_banzai
In this example, you could use either of these commands to stop the container:
1# Using the ID2docker stop c1f2b80c13ae3# Using the Name4docker stop youthful_banzai
If you'd like to further abstract away the process of building and running the Stoplight Docker image, you have the option of configuring and using Docker Compose to simplify the process and reduce friction for other team members.
First, create a new docker-compose.yml
file:
touch docker-compose.yml
and paste in the following contents:
1# docker-compose.yml2version: '3'3services:4prism:5image: stoplight/prism:46ports:7- '4010:4010'8command: mock -h 0.0.0.0 https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json
You can now run the Dockerized Prism container with an even shorter command:
1docker-compose up -d2# compare to:3# docker run --init -d -p 4010:4010 stoplight/prism:4 mock -h 0.0.0.0 https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json
The Docker container will now be running and accessible on localhost:4010
just as before, and you can run the node example (node example.js)
once more to verify.