Part 2 - Specifications

Welcome to the second installment of our three part series about the MLS API. Check out the last post if you missed it.

Until recently, the MLS API required verbose, repetitive code to define, sanitize, and validate new routes. As routes were added over time, we ended up with a variety of methods to validate route parameters and then transform them to the types required by the data lookup methods. The process was repetitive and prone to inconsistencies and copy paste errors, such as pagination parameters being cast to numbers in all but two routes.

To solve this problem, we created the respectify module. Respectify acts as a restify middleware and it allows us to define route parameters and corresponding validation requirements along side the the top level route declaration.

Here is how we would add it to our restify server:

var restify = require('restify')
  , Respectify = require('respectify')
  , server = restify.createServer()
  , respect = new Respectify(server)

// Add the middleware for sanitization / validation
server.use(respect.middleware)

Specifications

Building a spec can be apprached in many ways. The approach we took respectify was to define the spec directly in code as part of the route definition. We prefer writing code over the external JSON or XML configuration file approach. We felt that a code first approach allowed us to continue developing the API as we normally would, adding in extra information, as opposed to learning an entirely new system of route creation.

The result is a params object on the route that explains exactly what method parameters can be sent, and what valid values are. An example route might look something like this:

server.get({
  path: '/users'
, version: '1.0.0'
, description: 'user lookup route'
, params: {
    username: {
      dataTypes: 'string'
    , desc: 'username lookup'
    }
  , page: {
      dataTypes: 'number'
    , desc: 'current page'
    , default: 1
    }
  , pagesize: {
      dataTypes: 'number'
    , desc: 'page size'
    , default: 50
    , min: 1
    , max: 100
    }
  }
}, function(req, res, next) {
  // req.params will only contain properties as defined in the `params` object above
  res.send(200, ...)
})

In the example above, we defined three parameters, each with a data type, a description, default values, and, for number values, a max and min. Respectify allows us to add the parameter specification along side the route, giving us to use a single, predictable place (the route definition file) to modify parameters and validation for all routes.

Respectify can also be used to extract information about the route configuration at runtime. This data can be useful in many ways. We use it to drive automated documentation generation, a topic that will be covered in detail in the following post.

Here is a look at what respectify can give us back for route information:

[{
  "route": "/users",
  "parameters": [
    {
      "name": "username",
      "required": false,
      "paramType": "querystring",
      "dataTypes": [
        "string"
      ],
      "description": "username lookup"
    },
    {
      "name": "page",
      "required": false,
      "paramType": "querystring",
      "dataTypes": [
        "number"
      ],
      "default": 1,
      "description": "current page"
    },
    {
      "name": "pagesize",
      "required": false,
      "paramType": "querystring",
      "dataTypes": [
        "number"
      ],
      "min": 1,
      "max": 100,
      "default": 50,
      "description": "page size"
    }
  ],
  "method": "GET",
  "versions": [
    "1.0.0"
  ]
}]

This is really just the route object transformed into JSON, making it easy to access programmatically, or even to send directly to API clients.

Another interesting thing we can do is put the OPTIONS method to good use, sending back the route specification for any defined route.

Here is what that might look like as restify middleware:

server.opts({path: /.+/, version: '1.0.0'}, function(req, res, next) {
  var _method = req.method
    , router = server.router
    , methods = ['GET', 'POST', 'PUT', 'HEAD', 'DELETE']

  // Intended to represent the entire server
  if (req.url.replace('/', '') === '*') {
    return this.returnRoutes(req, res, next)
  }

  // Iterate through all HTTP methods to find possible routes
  async.mapSeries(methods, function(method, cb) {

    // Change the request method so restify can find the correct route
    req.method = method

    // Find the restify route object
    router.find(req, res, function(err, route, params) {
      if (err && err.statusCode !== 405) return cb(err)
      if (!route) return cb(null)
      return cb(null, respect.getSpecByRoute(route))
    })
  }, function(err, resp) {
    // Revert to the original method
    req.method = _method

    // Make sure a valid route was requested
    if (err || !resp || !resp.length) {
      return next(new restify.ResourceNotFoundError())
    }

    // Filter out all undefined routes
    var routes = resp.filter(function(x) { return !!x })

    if (routes.length) {
      return res.send(200, routes)
    }

    // No routes were found
    res.send(404)
  })
})

Now, for any route that exists on the server, a client may send an OPTIONS request to get the exact specification for that route.

There are many other use cases for respctify, but the largest benefit for us has been reducing boilerplate code our codebase and providing a clear path forward for future development.

In the next post, we will wrap up with one of life’s greatest joys: documentation. We will show you how respectify can save you time while improving the accuracy of your docs.

New MLS Mobile App for 2015

January 12, 2015

Open beta for new MLSsoccer.com

December 04, 2014 Hans Gutknecht

Standings Visualizations

October 30, 2014 Tom Youds