Easypost Case Study
A case study is an simple example of how to use Sandbox to stub out a particular pattern or API. In this case study we are looking at the Easypost API.
Easypost (opens in a new tab) is a simple shipping API that aims to abstract all the nastiness of integrating with logistics carriers like USPS, UPS, FedEx etc. They are currently limited to US and Canadian providers, but their API is a good example of a fresh, simple take on a potentially complex API interaction.
Using Sandbox we can quickly create a mock of the Easypost API to test against, allowing us to decouple development dependencies and simplify testing.
The Easypost Getting Started guide (opens in a new tab) describes the process best, but creating a new Easypost shipment consists of these 5 steps:
- Create a From Address - HTTP POST /v2/addresses
- Create a To Address - HTTP POST /v2/addresses
- Create a Parcel - HTTP POST /v2/parcels
- Create a Shipment and Get Rates - HTTP POST /v2/shipments
To support these API calls we are going to need to build 4 new routes, one for each route listed above (reusing the same service for From and To addresses). There will 3 different maps to store the different Address, Parcel and Shipment objects these routes will create. For example when a new Address object is created, the stub will create a new Address object, assign the various values to it and generate a new 'adr_xxx' ID and store it in the state.addresses map to be retrieved later on.
- Step 1 - Setup The first step is to create the three persistent maps that we are going to use to store our objects, and create three helper functions that will be reused throughout the mock service. Most of the helper functions are straightforward, with the lookupObject being slightly more tricky and using a throw() function to flag when a given object is not found.
Note: the current timestamp is generated using the excellent Moment.js library for JavaScript, as the standard JS date/time libraries are lacking in functionality.
var moment = require('moment'); // require
//persistent storage
state.addresses = state.addresses || {}
var addressMap = state.addresses
state.parcels = state.parcels || {}
var parcelMap = state.parcels
state.shipments = state.shipments || {}
var shipmentMap = state.shipments
//helpers
function lookupObject(id, map){
var result = map[id]
if(result == undefined){
throw({message: "Can't find " + id})
}
return result
}
function getId() {
var s4 = function() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
return s4() + s4();
}
function getCurrentTimestamp(){
return moment().format("YYYY-MM-DDTHH:mm:ss[Z]")
}
- Step 2 - Define GET and POST routes
The next step is to define all the routes, we need one GET to retrieve and one POST to create each of the Address, Parcel and Shipment objects.
The 3 GET requests are obviously quite simple, they just try and retrieve the given object from the associated persistent map (address, parcel or shipment) and fail if no value is found. The POST requests are a bit more complicated, in the Easypost API the expected inbound request is made up of x-www-form-urlencoded values rather than a more simple JSON style request. This means we need to map these values from their form-style request into a JSON object before it is stored.
//routes
mock.define('/v2/addresses/:addressId','get', function(req, res){
try{
res.status(200).json(lookupObject(req.params.addressId, addressMap))
} catch(e) {
res.status(400).json({ error: { message: e.message } })
}
})
mock.define('/v2/addresses','post', function(req, res){
var address = {
id: "adr_" + getId(),
object: "Address",
name: null,
street1: req.body['address[street1]'],
street2: req.body['address[street2]'],
city: req.body['address[city]'],
state: req.body['address[state]'],
zip: req.body['address[zip]'],
country: req.body['address[country]'],
phone: req.body['address[phone]'],
email: req.body['address[email]'],
created_at:getCurrentTimestamp(),
updated_at:getCurrentTimestamp()
}
addressMap[address.id] = address
console.log("Added address with id: " + address.id)
res.status(200).json(address)
})
mock.define('/v2/parcels/{parcelId}','get', function(req, res){
try{
res.status(200).json(lookupObject(req.params.parcelId, parcelMap))
} catch(e) {
res.status(400).json({ error: { message: e.message } })
}
})
mock.define('/v2/parcels','post', function(req, res){
var parcel = {
id: "prcl_" + getId(),
object:"Parcel",
length: Number(req.body["parcel[length]"]),
width: Number(req.body["parcel[width]"]),
height: Number(req.body["parcel[height]"]),
weight: Number(req.body["parcel[weight]"]),
predefined_package: req.body["parcel[predefined_package]"],
created_at:getCurrentTimestamp(),
updated_at:getCurrentTimestamp()
}
parcelMap[parcel.id] = parcel
console.log("Added parcel with id: " + parcel.id)
res.status(200).json(parcel)
})
mock.define('/v2/shipments/{shipmentId}','get', function(req, res){
try{
res.status(200).json(lookupObject(req.params.shipmentId, shipmentMap))
} catch(e) {
res.status(400).json({ error: { message: e.message } })
}
})
mock.define('/v2/shipments','post', function(req, res){
var shipmentId = "shp_" + getId();
try {
var shipment = {
id: shipmentId,
object:"Shipment",
mode:"test",
to_address: lookupObject(req.body["shipment[to_address][id]"], addressMap),
from_address: lookupObject(req.body["shipment[from_address][id]"], addressMap),
parcel: lookupObject(req.body["shipment[parcel][id]"], parcelMap),
customs_info: undefined, //req.body["shipment[customs_info][id]"],
predefined_package: req.body["shipment[predefined_package]"],
rates:[
{
id: "rate_" + getId(),
object: "Rate",
service: "FirstClassPackageInternationalService",
rate: "9.50",
carrier: "USPS",
shipment_id: shipmentId,
created_at: getCurrentTimestamp(),
updated_at: getCurrentTimestamp()
}
],
scan_form: null,
selected_rate: null,
postage_label: null,
return_label: null,
tracking_code: null,
refund_status: null,
insurance: null,
created_at:getCurrentTimestamp(),
updated_at:getCurrentTimestamp()
}
shipmentMap[shipment.id] = shipment
console.log("Added shipment with id: " + shipment.id)
res.status(200).json(shipment)
} catch(e) {
res.status(400).json({ error: { message: e.message } })
}
})
- Step 3 - Try it out
With these 6 routes added to a Sandbox stub, you can build up a shipment just like you would by hitting the real Easypost API.
curl -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "address[company]=EasyPost" \
-d "address[street1]=164 Townsend Street" \
-d "address[street2]=Unit 1" \
-d "address[state]=CA" \
-d "address[zip]=90277" \
-d "address[country]=US" \
http://easypost.mock.apigit.com/v2/addresses
This CURL requests creates a new Easypost Address object, generating a new Address ID reference that can be used later in the Shipment service for example.
{
"id": "adr_0c6fd1d3",
"object": "Address",
"street1": "164 Townsend Street",
"street2": "Unit 1",
"state": "CA",
"zip": "90277",
"country": "US",
"created_at": "2014-09-01T06:54:02Z",
"updated_at": "2014-09-01T06:54:02Z"
}