Wille Faler is an experienced software developer, architect and agile coach with experience across a number of different industries as an independent consultant. Wille specializes in backend, integration, and Java technologies, but has more recently found a passion for Scala and text mining/analysis. Wille is a DZone MVB and is not an employee of DZone and has posted 39 posts at DZone. You can read more from them at their website. View Full User Profile

API Design For API Consumers vs. API Implementors

02.01.2011
| 9221 views |
  • submit to reddit

I have designed a few API’s in my days, but during my work with Bowler, a few things occurred to me: creating API’s for API consumers vs. API’s for others to implement are slightly different beasts, not by much, but there’s still a difference.

You generally find two types of API’s:

  • Those that people will simply use, but not really extend - API’s that are consumed in a simple manner.
  • API’s that are meant to be implemented, but not necessarily consumed by the same developers. This would typically be things like pluggable behavior in a wider framework.
In the first case, you want to make the API simple and straightforward to use, an example of this would for instance be the Renderable, Validations and ParameterMapper traits of the Bowler API as shown in the code below:
package org.bowlerframework.examples

import org.bowlerframework.controller.Controller
import org.bowlerframework.model.{ ParameterMapper, Validations}
import org.bowlerframework.view.{Renderable}

/**
* Our main application controller, showing a simple CRUD interface.
*
* extends:
* - Controller: used to construct routes and deal with them by providing functions that respond to routes.
* - ParameterMapper: takes a request and maps any values into beans or other objects.
* - Validations: validation enables the Controller
* - Renderable: allows you to render View Model objects.
*/

class WidgetController extends Controller with ParameterMapper with Validations with Renderable {


  // simple, no args render, just renders the root (or http 204 for JSON)
  get("/")((request, response) => render)

  def renderWidgets = {
    val widgets = Widgets.findAll
    if(widgets.size == 0)
      render
    else
      render(widgets)
  }

  // renders the base routes
  get("/widgets")((request, response) => renderWidgets)
  get("/widgets/")((request, response) => renderWidgets)
  get("/composable")((request, response) => renderWidgets)
  get("/composable/")((request, response) => renderWidgets)


  // responds to GET with a named parameter (:id becomes named to id)
  get("/widgets/:id")((request, response) => {
    this.mapRequest[Option[Widget]](request)(widget => {
      if(widget != None)
        render(widget.get)
      else render
    })
  })

  // responds to HTTP DELETE with named param
  delete("/widgets/:id")((request, response) => {
    this.mapRequest[Option[Widget]](request)(widget => {
      if(widget != None)
        Widgets.delete(widget.get)
      else
        response.sendError(500)
    })
  })

  // retrieves an edit form for a widget that lets you edit a pre-existing widget.
  get("/widgets/:id/edit")((request, response) => {
    mapRequest[Option[Widget]](request)(widget => {
      if(widget != None)
        render(widget.get)
      else render
    })
  })

  // form for creating a new Widget - passes in a new, empty widget to be filled out.
  get ("/widgets/new")((request, response) => {render(Widget(0, null, null, null))})

  // HTTP POST for creating new Widgets.
  post("/widgets")((request, response) =>{

    // Map the request into the resulting Widget..
    this.mapRequest[Widget](request)(widget => {

      // validate the Widget, pass the widget as a param, so the validation error functionality knows which object
      // failed validation, IF it fails.
      validate(widget){
        val validator = new WidgetValidator(widget)
        validator.add(new UniqueValidator({widget.id}))
        validator.validate
      }
      Widgets.create(widget)
      // send a redirect to the base page.
      response.sendRedirect("/widgets")
    })
  })


  // similar to the above POST, but for updating existing widgets.
  post("/widgets/:id")((request, response) => {
    this.mapRequest[Widget](request)(widget => {
      validate(widget){
        val validator = new WidgetValidator(widget)
        validator.validate
      }
      println("update")
      Widgets.update(widget)
      response.sendRedirect("/widgets")
    })
  })

}

These API’s are there to make life easy for application developers, and are simply “consumed”/used, but I don’t foresee a lot of cases where a developer would actually want to or need to extend them in any way. 

The flipside of this is API’s that are meant to be implemented, but rarely seen by the application developer. In Bowler, an example of this is the RenderStrategy trait and the ViewRenderer that it returns:
package org.bowlerframework.view

import org.bowlerframework.{Request, Response}

/**
 * Strategy for resolving a ViewRenderer given a request
 */

trait RenderStrategy{
  def resolveViewRenderer(request: Request): ViewRenderer
}
A RenderStrategy in Bowler is responsible for inspecting a Request and chosing a ViewRenderer (something that knows how to render a response) based on the Requests characteristics, such as for example the “Accept” HTTP Headers. A RenderStrategy and ViewRenderers are easily pluggable and configurable behaviors that most developers will likely never need- or want to touch. But the option for customization is there - yet once the customization is there, it is generally not seen again unless there is an issue related to it.

Ease of Use vs. Ease of Implementation
One very clear example of the distinction between ease of use for API’s for consumption vs. ease of implementation is the previously mentioned Renderable trait: Renderable’s “render” function takes a vararg argument of the Resource View Model an application developer would want to render. Varargs can be great and make things very easy to use from an application developes point of view.
However, varargs are hellish and fraught with complications for someone implementing based on an API, hence varargs do not turn up in any Bowler traits which are primarily mean to be implemented, it is only used in Renderable for ease of use for application developers.

What I wanted to illustrate with these examples is simply the fact that as a API designers, we have to consider the use case for an API, will people simply “consume” it, or will they primarily implement it? This fundamental question may and perhaps should impact your design in subtle ways which may have a larger impact down the road than you ever thought.


References
Published at DZone with permission of Wille Faler, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)